Skip to content

Commit be318db

Browse files
refactor(plugin-security): move SecurityPlugin's Layer 0 tenant write wall into security-plugin-write-tenant-wall.ts (#25223 cut 5) (#25378)
<!-- 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 5 of the `security-plugin.ts` split: step 3.7's Layer 0 tenant write wall. The claim is 6357083478. The body of one async method, `composeWriteTenantWall`, moves unedited to the new module `packages/plugins/plugin-security/src/security-plugin-write-tenant-wall.ts`. It composes the wall once for one write: - the ADR-0123 D2 scope pre-test, which refuses a write when the context has no active organization, ordered ahead of the forge guard; - the parts every row is held to: the caller's Layer 0 (ADR-0095 D1) and, on an on-behalf-of write, the delegator's (ADR-0090 D10); - the judgements that read those parts: the payload half, and the stored-image seam with its landing refusals (#24784, #24821) and `judgesOrganization` (#24822). Nothing in it logs or throws. The middleware's step 3.7 logs and throws each refusal through `raiseWriteWallRefusal`, and the write preview `writeWallVerdict` answers it as a row's verdict. Both stay in the class and still call the method. It is move-only, and the card stays open for the later cuts. Branch: `claude/project-thread-3fzya4`, the harness-pinned branch for this session, restarted from `origin/main` at `4d7e2b1f38` (cut 4's merge). It names no issue, so the claim comment is the identity record. **Why now.** Ruling 6353073966 item 4 held this body with the two read bodies until the Tier H docs PR for #25314 landed. That PR, #25316, merged as `97967a9a06`, and cut 4 moved the two read bodies. On this base, ADR-0123:4 anchors `security-plugin.ts#composeWriteTenantWall`, a method name. The method stays as a shell, so that anchor still resolves on its declaration. **What stays in `security-plugin.ts`.** - No module-level declaration moves. The `WriteTenantWall` and `WriteWallRefusal` interfaces keep their one declaration, because the method's signature and `raiseWriteWallRefusal` read them. The module names both through the class's own signature, as cuts 2 to 4 name `CbpRelation`. - `carriesActiveOrganization` stays, because `callerHasOrganizationScope` reads it too. It gains `export`, and the module imports it by value, as cut 3's module imports its five values. It is read only inside the function body, at call time, never while either module evaluates. `index.ts` still exports only `SecurityPlugin`, so the published surface does not move (measured below). - The `matchesFilterCondition` name leaves the `@objectstack/formula` import of `security-plugin.ts`, because no code left there reads it. The module imports it from the same package. The module also imports `postureUsesUnionScope` from `@objectstack/spec/security` and `PermissionDeniedError` from `errors.js`, as `security-plugin.ts` does. **One reader outside the package moves with the body.** `docs/qa/platform-checklist/areas/access-security.json` cites `packages/plugins/plugin-security/src/security-plugin.ts#denyTenantPlacement` twice, in the `fixtures.requires` prose of items `access-security.activation-write-operator-gate` and `access-security.walled-environment-metadata-operator-only`. `denyTenantPlacement` is a local `const` inside the moved body. With the body moved and that citation unchanged, `check-platform-checklist` exits 1 with ABSENT SYMBOL, measured on a trial before the commit. Both citations now name `security-plugin-write-tenant-wall.ts#denyTenantPlacement`. Only the path changed: the symbol, the quoted 403 text and the rest of both strings are as they were. The gate exits 0 at head. ## The published `.d.ts` does not change Under the series bar (ruling 6348891160 item 1), measured on base `4d7e2b1f38` 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: `ea54b6c9538b135fee36e1309fddc763113443c0dddd174950cbd96c290536a2`. The raw diff is 0 lines, so no union order moved either. - Normalised sha256, all four the same: `652f26b24f41bf031d428f708685abb7517d4247a765246d153d2bbeac13e444`. - The hashes differ from cut 4's report because `origin/main` moved between cut 4's head and its merge. The only lines that differ from cut 4's head `.d.ts` are six inlined `requiresFeature` unions from `@objectstack/spec`, which gained `"cloudManaged"`. Base and head here are both built against `4d7e2b1f38`'s dependencies. - 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 1794 (`private composeWriteTenantWall;`): - Made public (`composeWriteTenantWall;`): raw `5c1fa0e3230960f533466bb151a1e1769cdcc855d3227f776a9832d6f0fb7195`, normalised `4defa680f3cf4b2ed2b0c60696d8bea7031a82edd43bcabc4ad4b2e346727c80`. - Renamed (`private composeWriteTenantWallRenamed;`): raw `82704c4f85cb5d28cb672046ef7e2b05da8348c6cb4efccd4e0e5fe7e58eb0dc`, normalised `614cf491f999aa7a4fde4a3317cec1b1ee9e95306e29dab7c5f1c452b9982870`. - 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 (`require` and `import()` of the built bundles), not parsed. The sorted name list hashes `f607fd765d6b277e08b3b46ccb16d1e2d133ec55658ffe40155aef25e1bf54f0` on all four, the same as at cuts 2 to 4. The new `carriesActiveOrganization` export of `security-plugin.ts` does not reach the entry. - The bundle JS bytes differ (`index.js` `53af1376` to `a0e03516`, `index.mjs` `7beda6f7` to `a75436eb`), because code moved within them. No changeset: `skip-changeset`, per ruling 6348891160 item 4. ## What moved The base line numbers are the method's signature and closing brace at `4d7e2b1f38`. The last column lists the members the body reads, which is exactly what its host carries. | method | base lines | host members | |---|---|---| | `composeWriteTenantWall` (async) | `:8408`-`:8746` | `computeWriteTenantCheckFilter`, `computeWriteTenantWall`, `orgScopingEnabled`, `organizationWallRefusal`, `tenancyPosture` | The stub drops `async` and returns the moved function's promise, as cuts 1 and 3 did for their async methods (6 and 5 of them). The `.d.ts` emits a private member without modifiers, so this does not reach it. ## The shape (#23050 shape B, as cuts 1 to 4 applied it) - The class keeps the private method with its name, signature, docblock and position, because tsup emits private members with their JSDoc into the `.d.ts`. The body is now one host literal plus `composeWriteTenantWall.call(host, …)`, marked `[#25223 cut 5]`. - `WriteTenantWallHost` types every member as the class member itself (`SecurityPlugin['m']`). The moved function types its `this` as the `Pick` of the 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. - The module imports `SecurityPlugin` type-only. It names `WriteTenantWall` as the awaited return type of `SecurityPlugin['composeWriteTenantWall']`, and `WriteWallRefusal` as the non-null type of that wall's `preTest`. - 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 pre-test is still `organizationWallRefusal`, the one verdict `canWriteObject` asks too. The caller's and the delegator's Layer 0 still come from the read side's `computeLayeredRlsFilter`, through `computeWriteTenantWall` and `computeWriteTenantCheckFilter`. No moved body calls a function-valued field, so no receiver changes. - Fields go as getters. `tenancyPosture` is not declared `readonly`, because `start()` resolves it after construction. `orgScopingEnabled` is the class's own getter over it, handed over as cut 1 handed it. The host declares both `readonly`, so the moved body cannot assign a member through it; the typecheck passes, so it does not. - Some reads of `this.tenancyPosture` happen when a returned judgement is called, not when the wall is composed: the developer message of `denyOrganizationRemoval`, which `judgePayload` and `judgeRow` reach, and the developer messages of `judgeLanding`'s two refusals. `this` inside those arrow functions is the host, and the host's getter reads the plugin's field at that call, which is the read the method made before. - Nothing re-exports the module, so the package entry does not move. ## Proof of move-only - **Body, by AST token**: the moved function's body has the same TypeScript token sequence as the base method body, and each of its 329 lines is the base line with two leading spaces removed. Parameters, the return type and `async` match. A negative control, `rows.some(emptiesOrganization)` changed to `!rows.some(emptiesOrganization)` in the payload half, fails the check. - **Class side**: the method keeps its docblock and signature, less `async`. Every other class member, 124 named members plus the constructor, is byte-identical to base, `organizationWallRefusal`, `computeWriteTenantWall`, `computeWriteTenantCheckFilter`, `raiseWriteWallRefusal`, `writeWallVerdict` and `start()` included. The class has 126 members at base and at head. - **Top level**: 100 of the 102 top-level statements other than the class are byte-identical to base, `WriteTenantWall` and `WriteWallRefusal` included. The two that differ are the `@objectstack/formula` import, which loses `matchesFilterCondition`, and `carriesActiveOrganization`, which is the base text with `export ` before `function`. The one new statement is the import of the new module. - **git's own view** (`git diff --color-moved=plain --color-moved-ws=allow-indentation-change 4d7e2b1 HEAD` over the two source files): 328 of 331 deleted lines and 337 of 453 added lines are detected as moved. - Deleted, not moved (3): the `matchesFilterCondition,` import line, the `function carriesActiveOrganization(` line, which comes back with `export`, and the method's signature line, which comes back without `async`. - Added, not moved: those two lines' replacements, the stub body, and the import of the new module with its comment. In the module: the license header, the module docblock, the imports, the two type aliases, `WriteTenantWallHost`, the pointer docblock and the rewritten signature head. The rest are lines git does not pair with a moved block, such as blanks and closing braces. - **Size**: 795 changed lines (+462 / -333, 4 files), under the 3000-line human-merge threshold. `security-plugin.ts` goes from 10,205 to 9,890 lines. ## ADR anchors (ruling 6353073966 item 2) - `node scripts/check-adr-symbol-anchors.mjs`: exit 0 at base `4d7e2b1f38` and exit 0 at head, both with `2383 anchors across 142 records resolve — 466 symbol (440 declaration, 26 literal), 1889 file-level, 28 cross-repo, 6 exempt, 18 continuation`, the same counts as cut 4. CI runs this gate (`lint.yml`), 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, 79 anchored files (78 at base, plus the new entry). The new entry is `scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-write-tenant-wall.ts.json`. It carries ADR-0123 and the ADR-0123 D2 paragraph of the `security-plugin.ts` entry, verbatim: the no-active-organization refusal is this body's pre-test, and the paragraph's rule that it stays ordered ahead of the forge guard is about code that now lives in this module. The moved comments also cite ADR-0090, ADR-0095, ADR-0105, ADR-0112 and ADR-0131. Like the `security-plugin.ts` entry, the new entry does not list every ADR its file cites, and the gate passes. The `security-plugin.ts` entry is unchanged: the middleware's step 3.7, which throws that refusal, stays there. `docs/adr/**` is not touched. - The anchor map covers every ADR symbol anchor naming `security-plugin.ts`: 27 anchors, 18 symbols, 10 ADRs on this base. It was measured with `extractAnchors` and `symbolSegmentResolution` from `scripts/symbol-anchors.mjs`, which is measurement, not a new gate. It was read on `4d7e2b1f38` BEFORE the cut, with the body 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 line it resolves on, and the member that line sits in. Every anchor stays in `security-plugin.ts`. | anchor | cited at | base class · line (member) | head class · line | |---|---|---|---| | `#CBP_MAX_CHAIN_DEPTH` | ADR-0055:135 | declaration · `:657` (top-level) | declaration · `:660` | | `#SecurityPlugin` | ADR-0049:49, ADR-0138:248, ADR-0138:265 | declaration · `:1334` (class) | declaration · `:1337` | | `#SecurityPlugin.getMetadataReadableFields` | ADR-0138:153 | declaration · `:2483` (`start`), `:6102` (method) | declaration · `:2486`, `:6105` | | `#SecurityPlugin.resolvePermissionSetsForContextUnmemoized` | ADR-0138:427 | declaration · `:7013` (method) | declaration · `:7016` | | `#assertControlledByParentWrite` | ADR-0055:58, ADR-0055:125 | declaration · `:9462` (method shell) | declaration · `:9147` | | `#assertMasterRowEditable` | ADR-0055:125 | declaration · `:9588` (method shell) | declaration · `:9273` | | `#collectRLSPolicies` | ADR-0105:93 | declaration · `:9642` (method) | declaration · `:9327` | | `#composeWriteTenantWall` | ADR-0123:4 | declaration · `:8408` (method) | declaration · `:8411` (method shell) | | `#computeControlledByParentFilter` | ADR-0055:5, ADR-0055:123, ADR-0139:246 | declaration · `:9054` (method shell) | declaration · `:8739` | | `#computeDelegatedReadFilter` | ADR-0139:223 | declaration · `:5980` (method) | declaration · `:5983` | | `#computeRlsFilter` | ADR-0055:24 | declaration · `:5232` (`explainAccessForCaller`), `:7635` (method) | declaration · `:5235`, `:7638` | | `#getReadFilter` | ADR-0096:181, :182, :183, :199, ADR-0139:222 | declaration · `:2246` (`start`), `:5858` (method) | declaration · `:2249`, `:5861` | | `#hasPlatformAdminPosture` | ADR-0099:317 | declaration · `:7662` (method) | declaration · `:7665` | | `#isPrincipalLessContext` | ADR-0138:152 | declaration · `:511` (top-level) | declaration · `:514` | | `#manage_metadata` | ADR-0099:68 | literal · `:260` (`PLATFORM_ADMIN_ONLY_CAPABILITIES`) | literal · `:263` | | `#nav_permission_sets` | ADR-0126:363 | literal · `:1739` (`init`) | literal · `:1742` | | `#rlsFilter` | ADR-0055:24 | declaration · `:4309` (local in `start`) | declaration · `:4312` | | `#writeCheckPolicies` | ADR-0066:112 | declaration · `:1229` (top-level) | declaration · `:1232` | No resolving site moves. ADR-0123:4 resolves on the `composeWriteTenantWall` declaration, which stays as a shell. The new module also declares a function of that name, but the anchor names `security-plugin.ts`, so it is judged there. This cut adds no host-literal key line for an anchored name. ## Readers One reader of the moved lines exists outside this package, the platform checklist above, and it is re-pointed in this PR. No test, script, gate, baseline or other docs anchor reads the moved lines by path. - **The platform checklist.** `check-platform-checklist` exits 0 at base and at head. Both print `symbol anchors: 986/996 resolved … 10 on the named #16898 residual, 18 file floors held`. The cited-source count goes from 435 to 436, because the new module is now a cited source. The scoped run `--changed-from origin/main` judged 7 anchors in 3 anchored files of the change set, and lost 0. Before the re-point, on a trial of the same move, the full gate exited 1 with ABSENT SYMBOL for both citations. - **The census pages.** The body holds no `isSystem` read. One comment names `isSystem`, which the census does not count. So `check-system-context-census` has no row to move, and `content/docs/permissions/system-context.mdx` is not touched. `node scripts/isystem-census.mjs --json` gives the same 148 sites at base and head (12 in `security-plugin*.ts`), compared without line numbers. Only the corpus and scanned file counts move, by one. The body holds no engine write, so `node scripts/tenant-audit-census.mjs --json` gives the same 237 write call sites at base and head, compared the same way. Only `scannedSources` moves, 712 to 713, plus the measured-at ref. - The other symbol-anchor gates all exit 0: `check-scripts-symbol-anchors`, `check-spec-docblock-symbol-anchors` and `pnpm --filter @objectstack/spec check:liveness`. - **Source scans by path.** Two tests read package sources by path. `packages/core/src/security/row-serving-door-exposure.pin.test.ts` classifies every caller of the read admission, and the body calls no admission, so the new file needs no row. Run on its own: 3 of 3 pass. `delegated-organization-membership.test.ts` counts `resolveDelegatorContext(` per source file, and the body has none, so the two counted files are still `explain-engine.ts` and `security-plugin.ts`. It is part of the plugin-security suite, which passes. - Readers by name, not by path. These were read and none needs a change: - Docblocks in `security-plugin.ts` itself name `{@link SecurityPlugin.composeWriteTenantWall}` (on `WriteWallRefusal` and `WriteTenantWall`, and in step 3.7 and `writeWallVerdict`). The method still exists under that name. - Test files name the wall's refusal sentences and its `[#24784]`, `[#24821]` and `[#24822]` arms, and assert behaviour, not location. The suite passes at head with the same counts as base. - Line citations: no `security-plugin.ts:N` citation in the repo points into the moved range, base `:8417`-`:8745`. - Head line numbers shift by +4 for base `:146`-`:186` (the new import), by +3 for base `:188`-`:8416`, and by -315 after `:8745`. Lines `:1`-`:145` do not move. The moved range becomes the 11-line stub at head `:8420`-`:8430`. - The citations `:132`, `:303`, `:309`, `:1614`, `:1690`, `:1736`, `:3215`, `:3281`, `:3519` and `:4344` are the same list cuts 2 to 4 read. Each lands in an unmoved or shifted range, and each already pointed at an unrelated line before cut 2, so this cut does not make any of them wrong. - Docs Drift Check, re-derived with `node scripts/docs-audit/affected-docs.mjs --json 4d7e2b1` (advisory): 0 pages. Its 17 anchors are the moved names and literals, and the one broad literal, `organization_id` (37 pages), is dropped as over-broad. - Census gates, run on base `4d7e2b1f38` and head and compared: - Identical output: `check-platform-object-tenancy-census`, `check-registry-log-declared`, `check-rest-log-spy-declared`, `check-system-context-census`, `check-tenant-audit-census`, `check-adr-symbol-anchors`, `check:cross-package-test-inputs`, `check:driver-memory-census` and `check:query-options-erasure`. The last reports the same counts at both, and `security-plugin.ts` is not a baseline key. - Only scanned counts move: - `check-undeclared-dep-imports`: 3298 to 3299 files, and 2837 to 2840 specifiers. The three are the new module's `@objectstack/spec/security` (type and value) and `@objectstack/formula` imports, both declared dependencies. - `check:doc-authoring`: 1417 to 1418 sources. - `check:org-identifier`: 3640 to 3641 files. - `check:logger-receiver-detach`: 3614 to 3616 files. - `check-position-name-fold-loaders`: 12316 to 12321 files, with the same 8 references (6 test, 2 declared instrument, 0 loader). It walks the working tree. Two of the five are this PR's new files. The other three are gitignored local build outputs that exist only in the head worktree (`packages/spec/json-schema/data/QueryPageWindow.json`, `QueryPageWindowParam.json` and `apps/docs/next-env.d.ts`), which CI's checkout does not have. The second extra file of `check:logger-receiver-detach` is the same kind of local output, as at cuts 2 to 4. - `check-issue-citations` judges a change set, so it has nothing to read at base. At head it judged 19 citations across 2 files, and all resolve. ## Acceptance notes - Nothing outside this cut is touched: no other section of the class, no behaviour, no `docs/adr/**`. The one file outside the package is the checklist, and only its two path strings change. - Serial check, also in the claim. #25355 touches `security-plugin.ts` with one import after `:47` and one block at `:1720`. #25323 (draft) touches `access-security.json` at `:2919`-`:3063` and adds no `#denyTenantPlacement` citation. This cut has no hunk near either. - Since the local runs, `origin/main` gained only #25362 (plugin-auth). It touches none of this cut's files and no reader of them, so the merge with this head is clean. - What is left of the tenant wall in `security-plugin.ts` after this cut: `computeWriteTenantCheckFilter`, `computeWriteTenantWall`, `organizationWallRefusal`, `raiseWriteWallRefusal`, `writeWallVerdict`, the two interfaces and the middleware's step 3.7 that calls the method. ## Gates All gates ran at the head, the tree of `765326ddf5`, on base `4d7e2b1f38`. - `pnpm --filter @objectstack/plugin-security test` (`vitest run --maxWorkers=3`, under the verify lock): Test Files 212 passed (212), Tests 4442 passed, 45 skipped (4487). Base `4d7e2b1f38` 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 two touched source 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 `4d7e2b1f38` first: on the dependency outputs of the previous base, the typecheck and build failed on two `@objectstack/core` exports that `bootstrap-platform-admin.ts` imports on `origin/main` and that the old core build lacked, not on this cut. - `packages/core/src/security/row-serving-door-exposure.pin.test.ts`: 3 of 3 pass. - `node scripts/check-adr-symbol-anchors.mjs` exit 0, `pnpm check:query-options-erasure` exit 0, and the symbol-anchor gates under Readers. None of these is derived for this change set; each was run by hand. - `dispatch-gates --commands`: 64 commands. They are cut 4's 62, plus `pnpm check:platform-checklist` and `pnpm --filter @objectstack/lint run check:doc-formula-expressions`, which the checklist file adds. The `--ran` reconciliation shows 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. The zero is derived: all 64 recorded an exit code and none is 3. Per command (exit code): - `node scripts/check-ci-filter-parity.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-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-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/spec run check:duration-unit-keys` 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:cross-package-test-inputs` exit 0 - `pnpm check:doc-authoring` 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: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:platform-checklist` exit 0 - `pnpm check:pnpm-filter-targets` exit 0 - `pnpm check:pnpm-flag-spellings` exit 0 - `pnpm check:published-files` exit 0 - `pnpm check:refd-timer-probe` 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: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 27b3f1f commit be318db

4 files changed

Lines changed: 462 additions & 333 deletions

File tree

‎docs/qa/platform-checklist/areas/access-security.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2579,7 +2579,7 @@
25792579
"stock showcase boots the `single` tenancy posture (no tenancy service / ADR-0093 D4-D5 fallback) — CONFIRM the posture before scoring, because it decides which gate can answer at all. No API exposes the posture; the proof is the boot banner line `Tenancy: single` (printed at every log level), corroborated at OS_LOG_LEVEL=info by the log line \"[security] tenancy posture 'single' — Layer 0 is inert …\"",
25802580
"a packaged flow: showcase_urgent_task_alert (com.example.showcase, examples/app-showcase/src/automation/flows/index.ts) for POST /api/v1/automation/showcase_urgent_task_alert/toggle",
25812581
"a packaged action for POST /api/v1/actions/_activation/showcase_task/showcase_mark_done (examples/app-showcase/src/ui/actions/index.ts)",
2582-
"a runtime-authored scratch permission set with system_permissions ['manage_metadata'] granted to a fresh member via a sys_user_permission_set row (same runtime-authoring pattern access-security.fls-mask-and-strip's read half uses) — no stock showcase set grants manage_metadata. The walled legs (knownGaps[0]; steps[8], acceptance[4]) need that set held by an organization OWNER inside the owner's own organization, and no HTTP door places that row on a walled boot (QA run #24783): the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, 403 '… organization_id is outside the caller's organization scope' (packages/plugins/plugin-security/src/security-plugin.ts#denyTenantPlacement), and the owner's own insert is refused by ADR-0090 D12 condition (c), because manage_metadata is declared scope 'platform' (packages/spec/src/security/capabilities.ts; packages/plugins/plugin-security/src/delegated-admin-gate.ts#organizationBoundaryFailure). Use the dogfood harness's recipe instead: the grant row {user_id, permission_set, organization_id} is a system-context engine insert stamped with that organization, as packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts#boot places it, or a direct row on the run's own scratch DB, as QA run #24783 placed it. Record it in the run record as a fixture write made outside HTTP"
2582+
"a runtime-authored scratch permission set with system_permissions ['manage_metadata'] granted to a fresh member via a sys_user_permission_set row (same runtime-authoring pattern access-security.fls-mask-and-strip's read half uses) — no stock showcase set grants manage_metadata. The walled legs (knownGaps[0]; steps[8], acceptance[4]) need that set held by an organization OWNER inside the owner's own organization, and no HTTP door places that row on a walled boot (QA run #24783): the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, 403 '… organization_id is outside the caller's organization scope' (packages/plugins/plugin-security/src/security-plugin-write-tenant-wall.ts#denyTenantPlacement), and the owner's own insert is refused by ADR-0090 D12 condition (c), because manage_metadata is declared scope 'platform' (packages/spec/src/security/capabilities.ts; packages/plugins/plugin-security/src/delegated-admin-gate.ts#organizationBoundaryFailure). Use the dogfood harness's recipe instead: the grant row {user_id, permission_set, organization_id} is a system-context engine insert stamped with that organization, as packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts#boot places it, or a direct row on the run's own scratch DB, as QA run #24783 placed it. Record it in the run record as a fixture write made outside HTTP"
25832583
],
25842584
"knownGaps": [
25852585
"The group/isolated legs (clause 5, acceptance[4]; steps[8]; variants[1]–[2]) run live on a walled boot, which a stock showcase boot is not (fixtures.requires[0] confirms `single`). On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). A run that does not provision it records those legs blocked(fixture) naming that entry, never fail. Clause 5 is also scored from the unit pins in automated.ref (oracle test), which drive the posture x identity matrix in-process. Never fake a posture to tick them.",
@@ -3550,7 +3550,7 @@
35503550
"requires": [
35513551
"a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)",
35523552
"a second boot with OS_TENANCY_POSTURE=isolated (walled posture) on its own port and file DB — the stock single posture leaves every walled clause vacuous, so confirm the posture from the boot log before scoring",
3553-
"on the walled boot: a second organization whose owner is granted manage_metadata but not the platform_admin position. No HTTP door places that grant on a walled boot (QA run #24783): the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, 403 '… organization_id is outside the caller's organization scope' (packages/plugins/plugin-security/src/security-plugin.ts#denyTenantPlacement), and the owner's own insert is refused by ADR-0090 D12 condition (c), because manage_metadata is declared scope 'platform' (packages/spec/src/security/capabilities.ts; packages/plugins/plugin-security/src/delegated-admin-gate.ts#organizationBoundaryFailure). The recipe is the dogfood harness's: the operator saves a permission set carrying systemPermissions ['manage_metadata'], and the grant row {user_id, permission_set, organization_id} is a system-context engine insert stamped with that organization, as packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts#boot places it, or a direct row on the run's own scratch DB, as QA run #24783 placed it. Record it in the run record as a fixture write made outside HTTP"
3553+
"on the walled boot: a second organization whose owner is granted manage_metadata but not the platform_admin position. No HTTP door places that grant on a walled boot (QA run #24783): the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, 403 '… organization_id is outside the caller's organization scope' (packages/plugins/plugin-security/src/security-plugin-write-tenant-wall.ts#denyTenantPlacement), and the owner's own insert is refused by ADR-0090 D12 condition (c), because manage_metadata is declared scope 'platform' (packages/spec/src/security/capabilities.ts; packages/plugins/plugin-security/src/delegated-admin-gate.ts#organizationBoundaryFailure). The recipe is the dogfood harness's: the operator saves a permission set carrying systemPermissions ['manage_metadata'], and the grant row {user_id, permission_set, organization_id} is a system-context engine insert stamped with that organization, as packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts#boot places it, or a direct row on the run's own scratch DB, as QA run #24783 placed it. Record it in the run record as a fixture write made outside HTTP"
35543554
],
35553555
"knownGaps": [
35563556
"Every live leg here needs the walled boot fixtures.requires[1] names and the second organization fixtures.requires[2] names. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). On that boot the second organization is created through POST /api/v1/auth/organization/create (RUNNER.md 'A stock `single` boot holds exactly one organization, and no door mints a second; a walled boot mints them through the auth API'). A run that does not provision them records acceptance[1], acceptance[2] and negative[0] blocked(fixture) naming those entries, never fail. steps[3]'s single-posture admit is only the control for the walled refusal and proves nothing alone. acceptance[0]'s pins boot the walled posture at kernel level and stay the item's pinned evidence"

0 commit comments

Comments
 (0)