Skip to content

Commit 3e51491

Browse files
refactor(plugin-security): move SecurityPlugin's controlled-by-parent write gate into security-plugin-cbp-write-gate.ts (#25223 cut 3) (#25324)
<!-- 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 3 of the `security-plugin.ts` split. The claim is 6354388643. The bodies of five async methods move unedited to the new module `packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts`: - `checkControlledByParentWrite` - `assertControlledByParentWrite` - `assertControlledByParentChainEditable` - `controlledByParentPredicateWriteMasters` - `assertMasterRowEditable` Together they are the ADR-0055 §3 controlled-by-parent write gate the write path runs at step 2.8, and the check the `security` service serves. The three module-level declarations only those bodies read move with them: `MASTER_EDIT_DENIAL_LEG`, `controlledByParentWriteOutcomeOf` and `writesMasterReference`. So do the four error classes only those bodies construct or test. 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 `21a25dd9f1` (cut 2's merge). It names no issue, so the claim comment is the identity record. **Five exports, said up front.** `security-plugin.ts` adds the `export` keyword to five module-level declarations, and the new module imports them by value. Each line is otherwise byte-identical to base, and `index.ts` re-exports only `SecurityPlugin`, so none reaches the package's surface (the export tables below are unchanged). They stay in `security-plugin.ts` for these reasons: - `CBP_MAX_CHAIN_DEPTH`: ADR-0055:135 anchors it by symbol in that file (ruling 6353073966 item 5), and `controlledByParentChainRefusal` there walks it too. - `requiredCapsForOperation`: `packages/spec/liveness/object.json` anchors it by symbol in that file, and that file belongs to the spec lane. The CRUD gate reads it too. - `PREDICATE_WRITE_READABLE_ID_CAP`, `predicateWriteReadableCapError` and `copyOfCallerPredicate`: `readableRowsScopeForPredicateWrite`, which stays, reads all three. Moving them would leave a value import running the other way. Each value is read only inside a function body, at call time, never while either module evaluates. So the two-way import is safe under ESM, and the published bundle holds one module. The plugin's 4403 tests run on the source modules, cycle included, and pass. These stay in `security-plugin.ts` byte for byte, by ruling item 4, until docs PR #25316 (card #25314) lands: `computeControlledByParentFilter`, `controlledByParentChainRefusal` and `composeWriteTenantWall`. ## The published `.d.ts` does not change Under the series bar (ruling 6348891160 item 1), measured on base `21a25dd9f1` and on the head (the tree of this PR's commit): - Raw sha256 of `dist/index.d.ts` and `dist/index.d.mts`, base and head, all four the same: `803f5bfb7da0b00f8cd8d8f25172463d8dfb07d93fbdd8a9c6f4d4b7435e429a`. The raw diff is 0 lines, so no union order moved either. - Normalised sha256, all four the same: `b6b5c1b7fa6fa188f6e2f7e4cca3816a246b570f20d708643d116012aa86fa77`. - 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`, line 2308 (`private assertControlledByParentWrite;`): - Made public (`assertControlledByParentWrite;`): raw `ea8bebdf8cd6c0e974393bf00a9434fce8ec2ba1c3cb816549fd203c39cb90c7`, normalised `1a405f50b386e1209593962447896d000b736456d5dcaa64ab8f9921522053bb`. - Renamed (`private assertControlledByParentWriteRenamed;`): raw `7a055168bef89d872b54102a60b735a6f12e7bf1af7f367ea67531e10b56654e`, normalised `5f730b82bac2ce457a64717cd57973fa75fcf06d0aee180f1b5cb86c8e4d83ab`. - Both differ from the real hashes, so the comparison does see a visibility change and a rename. - Export tables: the ESM and CJS bundles each export the same 63 names at base and head. The lists were loaded at runtime, not parsed. The sorted name list hashes `f607fd765d6b277e08b3b46ccb16d1e2d133ec55658ffe40155aef25e1bf54f0` on all four, the same as at cut 2. - The bundle JS bytes differ (`index.js` `6d2a398f` to `aece9708`, `index.mjs` `953c5d9a` to `38b8f218`), because code moved within them. No changeset: `skip-changeset`, per ruling 6348891160 item 4. ## What moved Base line numbers are each member's signature at `21a25dd9f1`. The last column lists the members the body reads, which is exactly what its host carries. | method | base lines | host members | |---|---|---| | `checkControlledByParentWrite` | `:9363`-`:9390` | `assertControlledByParentWrite`, `declaresControlledByParent`, `resolveOperationPrincipals` | | `assertControlledByParentWrite` | `:9525`-`:9780` | `assertControlledByParentChainEditable`, `controlledByParentPredicateWriteMasters`, `declaresControlledByParent`, `extractSingleId`, `readRowById`, `resolveCbpRelation` | | `assertControlledByParentChainEditable` | `:9804`-`:9899` | `assertMasterRowEditable`, `declaresControlledByParent`, `readRowById`, `resolveCbpRelation` | | `controlledByParentPredicateWriteMasters` | `:9937`-`:9986` | `ql` | | `assertMasterRowEditable` | `:10003`-`:10230` | `checkAuthoredRowWrite`, `collectRLSPolicies`, `computeRlsFilter`, `declaresControlledByParent`, `delegatorOwnWriteFloorOptions`, `getObjectSecurityMeta`, `permissionEvaluator`, `ql`, `resolveSharingCanEdit`, `resolveSharingWriteVerdict` | All five methods are async, and every moved function is async too. Module-level declarations that move, each with its docblock and verbatim: `MASTER_EDIT_DENIAL_LEG` (base `:659`, 11 lines with its docblock), `controlledByParentWriteOutcomeOf` (`:673`, 24 lines) and `writesMasterReference` (`:740`, 15 lines). No code left in `security-plugin.ts` reads them. Imports: `MasterDetailRelationMissingError`, `DetailRecordNotFoundError`, `MasterReferenceMissingError` and `MasterReferenceNotWritableError` leave the `errors.js` import of `security-plugin.ts`. No code there reads them any more. The module imports them, plus the names its bodies share with code that stays (`withoutOperationPrivateKeys`, the metadata-core engine dispatch resolvers, `PermissionSet`, the two contract types and `isPlatformOwnershipFloorPolicy`). Those stay imported in `security-plugin.ts` too. ## The shape (#23050 shape B, as cuts 1 and 2 applied it) - The class keeps each private method with its name, signature, docblock and position, because tsup emits private members with their JSDoc into the `.d.ts`. Each body is now one host literal plus `fn.call(host, …)`, marked `[#25223 cut 3]`. - `CbpWriteGateHost` types every member as the class member itself (`SecurityPlugin['m']`). Each moved function types its `this` as the `Pick` of the host over exactly the members its body reads, and each stub annotates its literal with the same `Pick`. There is no cast, and no member becomes public or protected. - The module imports `SecurityPlugin` type-only. `CbpRelation` keeps its one declaration in `security-plugin.ts`, and the module names it through the class's own signature, as cut 2's module does (an alias over the non-null return type of `SecurityPlugin['resolveCbpRelation']`). - 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 gate reaches. The bodies that call one another (the check and the gate, the gate and its chain walk, each leg and the master-row verdict) still do so through the plugin. No moved body calls a function-valued field, so no receiver changes. - Fields: `ql` and `permissionEvaluator` go as getters, because neither is declared `readonly`. `start()` assigns `ql` after construction, and `security-plugin.test.ts` replaces `permissionEvaluator` on a constructed plugin. The host declares both `readonly`, so a moved body cannot assign a member through it; the typecheck passes, so none does. - Async: every stub drops `async` and returns the moved function's own promise. Building the host reads no field and runs no getter, and calling an async function never throws synchronously, so a caller awaits a promise that settles exactly as the old method's did. - Nothing re-exports the module, so the package entry does not move. - The class docblocks keep their `{@link}` tags to the four error classes, byte for byte. Those names are no longer imported in `security-plugin.ts`, so an editor shows the links unresolved there. No gate resolves docblock links, and the `.d.ts` is unchanged. ## Proof of move-only - **Bodies, by AST token**: for all 5 methods, the moved function's body has the same TypeScript token sequence as the base method body, and each of its 617 lines is the base line with two leading spaces removed. Parameters, return types and `async` match. A negative control, `if (context?.isSystem)` changed to `if (!context?.isSystem)`, fails the check. - **Class side**: each method keeps its docblock and signature, with `async` dropped. The three moved declarations are verbatim in the module and absent from `security-plugin.ts`. Each of the five exported values is its base text with `export ` added and nothing else. `CbpRelation`, `computeControlledByParentFilter`, `controlledByParentChainRefusal` and `composeWriteTenantWall` are byte-identical to base. - **git's own view** (`git diff --color-moved=plain --color-moved-ws=allow-indentation-change 21a25dd HEAD` over the two source files): 673 of 683 deleted lines and 709 of 923 added lines are detected as moved. - Deleted, not moved (10): the five declaration lines that gain `export`, and the five `private async` signature lines that lose `async`. - Added, not moved: the replacements of those ten lines, the stub bodies, and the import of the new module with its comment. In the module: the license header, the module docblock, the imports, the `CbpRelation` alias, `CbpWriteGateHost`, one pointer docblock per function and the rewritten signature heads. The rest are lines git does not pair with a moved block, such as blanks and closing braces. - **Size**: 1613 changed lines (+930 / -683, 3 files), under the 3000-line human-merge threshold. `security-plugin.ts` goes from 10,823 to 10,208 lines. ## ADR anchors (ruling 6353073966 item 2) - `node scripts/check-adr-symbol-anchors.mjs`: exit 0 at base `21a25dd9f1` and exit 0 at head, both with `2383 anchors across 142 records resolve — 467 symbol (440 declaration, 27 literal), 1888 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, 77 anchored files (76 at base, plus the new entry). The new entry is `scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-cbp-write-gate.ts.json`. It carries ADR-0055 and the §1 invariant paragraph of the `security-plugin.ts` entry, verbatim, as cut 2's entry does. Its "writes throw MasterDetailRelationMissingError" half is the gate's relation leg, which now lives here. The moved comments also cite ADR-0066 and ADR-0090. Like the `security-plugin.ts` entry, which lists four of the many ADRs that file cites, the new entry does not list them, and the gate passes. The `security-plugin.ts` entry is unchanged, because the methods, their docblocks and the read half remain in that file. `docs/adr/**` is not touched. - The anchor map covers every ADR symbol anchor naming `security-plugin.ts`: 28 anchors, 20 symbols, 10 ADRs. It was measured with `extractAnchors` and `symbolSegmentResolution` from `scripts/symbol-anchors.mjs`, which is measurement, not a new gate. It was read on `21a25dd9f1` BEFORE the cut, with the five ranges blanked: 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, the code lines it resolves on, and the member each line sits in. The member is shown after the line number. Every anchor stays in `security-plugin.ts`. | anchor | cited at | base class · lines (member) | head class · lines (member) | |---|---|---|---| | `#CBP_MAX_CHAIN_DEPTH` | ADR-0055:135 | declaration · `:635` (top-level) | declaration · `:643` (top-level, now exported) | | `#SecurityPlugin` | ADR-0049:49, ADR-0138:248, ADR-0138:265 | declaration · `:1377` (class) | declaration · `:1332` (class) | | `#SecurityPlugin.getMetadataReadableFields` | ADR-0138:153 | declaration · `:2526` (`start`), `:6119` (method) | declaration · `:2481`, `:6074` | | `#SecurityPlugin.resolvePermissionSetsForContextUnmemoized` | ADR-0138:427 | declaration · `:7030` (method) | declaration · `:6985` (method) | | `#assertControlledByParentWrite` | ADR-0055:125 | declaration · `:9525` (method) | declaration · `:9465` (method shell) | | `#assertMasterRowEditable` | ADR-0055:125 | declaration · `:10003` (method) | declaration · `:9591` (method shell) | | `#collectRLSPolicies` | ADR-0105:93 | declaration · `:10260` (method) | declaration · `:9645` (method) | | `#computeControlledByParentFilter` | ADR-0055:5, ADR-0055:123, ADR-0139:246 | declaration · `:9071` (method) | declaration · `:9026` (method) | | `#computeDelegatedReadFilter` | ADR-0139:223 | declaration · `:5997` (method) | declaration · `:5952` (method) | | `#computeRlsFilter` | ADR-0055:24 | declaration · `:5249` (`explainAccessForCaller`), `:7652` (method) | declaration · `:5204`, `:7607` | | `#controlled_by_parent` | ADR-0055:58 | literal · `:9080` (`computeControlledByParentFilter`), `:9146` (`controlledByParentChainRefusal`) | literal · `:9035`, `:9101` | | `#getReadFilter` | ADR-0096:181, :182, :183, :199, ADR-0139:222 | declaration · `:2289` (`start`), `:5875` (method) | declaration · `:2244`, `:5830` | | `#hasPlatformAdminPosture` | ADR-0099:317 | declaration · `:7679` (method) | declaration · `:7634` (method) | | `#isPrincipalLessContext` | ADR-0138:152 | declaration · `:489` (top-level) | declaration · `:497` (top-level) | | `#manage_metadata` | ADR-0099:68 | literal · `:238` (`PLATFORM_ADMIN_ONLY_CAPABILITIES`) | literal · `:246` | | `#nav_permission_sets` | ADR-0126:363 | literal · `:1782` (`init`) | literal · `:1737` | | `#refuseNoActiveOrganization` | ADR-0123:4 | declaration · `:8492` (local in `composeWriteTenantWall`) | declaration · `:8447` | | `#rlsFilter` | ADR-0055:24 | declaration · `:4352` (local in `start`) | declaration · `:4307` | | `#sharingModel` | ADR-0055:26 | declaration · `:9079` (local in `computeControlledByParentFilter`) | declaration · `:9034` | | `#writeCheckPolicies` | ADR-0066:112 | declaration · `:1272` (top-level) | declaration · `:1227` (top-level) | No resolving site moves. The two anchors that name a moved method resolve on its declaration, which stays as a shell. `#collectRLSPolicies` and `#computeRlsFilter` are read by `assertMasterRowEditable`, but they resolve on their own declarations, which do not move. At head, four of these names appear on extra lines inside the stubs: `assertControlledByParentWrite`, `assertMasterRowEditable`, `collectRLSPolicies` and `computeRlsFilter`. Each is a key of a host literal (a line-start member key, so declaration-class) and a quoted member of the stub's `Pick` list (a literal). The class each anchor resolves with is the same at head as at base. The three anchors ruling item 4 holds for #25316 (ADR-0055:26 `#sharingModel`, ADR-0055:58 `#controlled_by_parent` and ADR-0123:4 `#refuseNoActiveOrganization`) still resolve only inside `computeControlledByParentFilter`, `controlledByParentChainRefusal` and `composeWriteTenantWall`. This cut does not touch those bodies. ## Readers One docs anchor reads a moved line by path: row 9b of `content/docs/permissions/system-context.mdx`, re-pointed below. No test, script, gate or baseline does. (Corrected by the seat; the report `6355294898` carries the same correction.) - **The census page, re-pointed in this PR.** `check-system-context-census` exits 1 on the cut without it. The served check's elevation read, `if (context?.isSystem) return { outcome: 'allow' };`, is the first line of `checkControlledByParentWrite`'s body, so it moved with the body. The census then finds that read in the new module with no row pointing at it (`file-without-a-row`). The page's row 9b still names `security-plugin.ts#checkControlledByParentWrite`, where the census no longer sees a read (`anchor-is-not-a-read-site`). - The fix is the second commit, `d87954919b`. Row 9b of `content/docs/permissions/system-context.mdx` now anchors `packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts#checkControlledByParentWrite`. That is a one-line change, and the row's prose and every other row are unchanged. The page is hand-written and not a governed surface. The claim's surface covers it: a docs anchor that reads a moved line by path moves in the same PR. - Base `21a25dd9f1` and head, compared with `node scripts/isystem-census.mjs --json`: the same 148 elevation read sites, the same symbols, receivers and text, once the one moved site's file is mapped back to `security-plugin.ts`. `nonElevationReads`, `roleCounts` and `staleLedgerRows` are identical. The gate then exits 0 at head with the same summary as base, except that the sites now span 70 files instead of 69. Its `--self-test` exits 0 too. - The other symbol-anchor gates all exit 0: `check-scripts-symbol-anchors`, `check-spec-docblock-symbol-anchors`, `pnpm --filter @objectstack/spec check:liveness`, and `check-platform-checklist --changed-from origin/main`. The last of these judged 6 anchors and lost 0. `packages/spec/liveness/object.json` anchors `security-plugin.ts#requiredCapsForOperation`, which stays. - Line citations: no `security-plugin.ts:N` citation in the repo points into a moved range. The moved ranges at `21a25dd9f1` are `:649`-`:684`, `:732`-`:746` and the five method bodies between `:9363` and `:10230`. - Head line numbers shift by +12 for base `:120`-`:167` (the new import), by +8 for base `:172`-`:648` (four error names left the `errors.js` import), by -29 for base `:686`-`:731` and by -45 for base `:747`-`:9362`. - The citations `:132`, `:303`, `:309`, `:1614`, `:1690`, `:1736`, `:3215`, `:3281`, `:3519` and `:4344` all land in those shifted ranges. They are the same list cut 2 read, in `CHANGELOG.md`, `docs/launch-readiness.md`, `docs/design/predicate-compilation-convergence.md`, `packages/objectql/src/engine.ts` and `packages/spec/liveness/object.json`. Cut 2's report found every one of them already pointing at an unrelated line, so this cut does not make any of them wrong. - Docs Drift Check, re-derived with `node scripts/docs-audit/affected-docs.mjs --json 21a25dd` (advisory): 4 pages. `system-context.mdx` is the page re-pointed above. `authorization.mdx`, `explain.mdx` and `rls.mdx` are reached only through the string literal `'not_applicable'` inside `checkControlledByParentWrite`. The literal moved unchanged and its behaviour is the same, so there is nothing to edit on those pages. - Census gates, run on base `21a25dd9f1` and head and compared: - Identical output: `check-platform-object-tenancy-census`, `check-position-name-fold-loaders`, `check-registry-log-declared`, `check-rest-log-spy-declared`, `check-tenant-audit-census` (237 write call sites at both), `check:cross-package-test-inputs`, `check:driver-memory-census` and `check:query-options-erasure`. The last of these reports the same counts at both, and `security-plugin.ts` is not a baseline key. The moved bodies carry no erasure site the baseline counts. - Only the scanned counts move, by the one new file: - `check-undeclared-dep-imports`: 3288 to 3289 files, and 2827 to 2831 specifiers. The 4 are the new module's package imports (`@objectstack/core`, `@objectstack/spec/security`, `@objectstack/metadata-core` and `@objectstack/spec/contracts`), all declared dependencies. - `check:doc-authoring`: 1411 to 1412 sources. - `check:org-identifier`: 3630 to 3631 files. - `check:logger-receiver-detach`: 3604 to 3606 files. The second extra file is `apps/docs/.next/types/validator.ts`, a gitignored local build output of the docs app, as at cut 2. It is not in the tree CI checks out. - `check-system-context-census`: compared above. - `check-issue-citations` judges a change set, so it has nothing to read at base. At head it judged 40 citations across 2 files, and all resolve. ## Acceptance notes - Nothing outside this cut is touched: no other section of the class, no behaviour, no other lane's file, and no `docs/adr/**`. - Serial check, also in the claim: of the open PRs, only #25311 touches `security-plugin.ts`, with one import after the `catalog-reference-report.js` import and one block inside `start()`. This cut has no hunk in either place. #25316 touches only `docs/adr/0055` and `docs/adr/0123`. - The next cut must not move `composeWriteTenantWall`, `computeControlledByParentFilter` or `controlledByParentChainRefusal` until #25316 lands (ruling item 4). ## Gates All gates ran at the head, the tree of `d87954919b`, on base `21a25dd9f1`. `origin/main` has not moved since that base. - `pnpm --filter @objectstack/plugin-security test` (`vitest run --maxWorkers=3`, under the verify lock): Test Files 210 passed (210), Tests 4403 passed, 45 skipped (4448). Base `21a25dd9f1` gives the same counts. VERDICT command-exit 0 on both. This ran on the cut commit `c3e6ac12cd`; the second commit changes only the docs page. - `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 two touched source files: exit 0. - `pnpm --filter @objectstack/plugin-security build`: exit 0. It produced the `.d.ts` and bundles measured above. - `node scripts/check-adr-symbol-anchors.mjs` exit 0, `pnpm check:query-options-erasure` exit 0, plus the three symbol-anchor gates under Readers. None of these is derived for this change set; each was run by hand. - `dispatch-gates --commands`: 92 commands on the 4 changed paths (62 for the cut alone; the docs page adds 30 docs-family gates). The `--ran` reconciliation shows 92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN. The zero is derived: all 92 recorded an exit code and none is 3. - Two docs-family gates first refused on a prerequisite, not a finding: `pnpm --filter @objectstack/spec run check:docs` exited 1 because the local `packages/spec/json-schema` tree was older than `packages/spec/src`, and `pnpm --filter @objectstack/spec run check:skill-examples` exited 3 because the local spec `dist` was stale. After `pnpm --filter @objectstack/spec build` (which left the tree clean), both exit 0. The list below records those re-runs. Per command (exit code): - `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: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: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: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:vendor-version-stamps` exit 0 - `pnpm check:watch-hint-literal` exit 0 - `pnpm check:query-options-erasure` exit 0 - `node scripts/check-adr-symbol-anchors.mjs` exit 0 - `node scripts/check-spec-docblock-symbol-anchors.mjs` exit 0 - `pnpm --filter @objectstack/spec check:liveness` exit 0 - `node scripts/check-platform-checklist.mjs --changed-from origin/main` 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 54df640 commit 3e51491

4 files changed

Lines changed: 931 additions & 684 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ that silently does not happen.
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` |
109109
| 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` |
110110
| 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.ts#explainAccessForCaller` |
111-
| 9b | The master-detail write check the `security` service serves (`checkControlledByParentWrite`, ADR-0055) answers `allow` | plugin-security | Get: `allow` for any record, with no master judged — the write path's own first exit, so a gate composing it admits exactly what the record's own system-context update would. Lose: nothing a caller reaches today: its two consumers, the `sys_attachment` and `sys_comment` parent gates, return before they ask for a system session | `packages/plugins/plugin-security/src/security-plugin.ts#checkControlledByParentWrite` |
111+
| 9b | The master-detail write check the `security` service serves (`checkControlledByParentWrite`, ADR-0055) answers `allow` | plugin-security | Get: `allow` for any record, with no master judged — the write path's own first exit, so a gate composing it admits exactly what the record's own system-context update would. Lose: nothing a caller reaches today: its two consumers, the `sys_attachment` and `sys_comment` parent gates, return before they ask for a system session | `packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts#checkControlledByParentWrite` |
112112
| 10 | Anonymous-deny treats the caller as authenticated | core | Get: passes the 401 seam with no `userId` | `packages/core/src/security/anonymous-deny.ts#shouldDenyAnonymous` |
113113
| 12 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `packages/plugins/plugin-auth/src/auth-plugin.ts#start` |
114114
| 13 | Per-request performance timings disclosed | observability | Get: timing headers a normal caller cannot pull | `packages/observability/src/perf-timing.ts#isPerfDisclosurePrincipal` |

0 commit comments

Comments
 (0)