Repository navigation
plugin-security injects org- and owner-shaped predicates onto federated external objects too — the #7738 defect on a layer outside objectql #7835
Description
Activity
Routing fix (route A) from the triage seat: appended
domain:identity— routing only; thefindinggrade is unchanged and no ownership is taken.Landing rationale (read from source at
origin/main@ 31fb03d): the named injector ispackages/plugins/plugin-security/src/rls-compiler.ts— confirmedcompileFilterbuildsRLSUserContext.organization_idfromexecutionContext.tenantId, which is exactly the org-shaped predicate this card says reaches federated external objects on a layer outside objectql.plugin-securityisdomain:identityper the lanes table. Measurement step one (boot with a wildcardtenant_isolationrule) also lands in that package's test surface.Dedup: sibling of #7738 (engine-layer fix in flight via PR #7833,
packages/objectql/src/engine.ts—hasTenantgate confirmed at the cited region) — deliberately out of #7738's lane per its dev report §4; not a duplicate of #7737/#7738. No other open card covers the plugin-security layer.Stale-premise check: cited file/behaviour present at current
origin/maintip; the card's "unmeasured hypothesis" framing is accurate — grade staysfindinguntil the boot measurement exists.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Triage grading (same round as the routing comment above): promoted
finding→pm:queue.Rationale, against the promote bar (bounded measurement with live-security stakes — same shape as #7823's promotion): the card names the exact injector (
plugin-security/src/rls-compiler.ts—compileFilterresolvingcurrent_user.organization_idfromexecutionContext.tenantId, confirmed atorigin/main@ 31fb03d), the exact two boot conditions to measure (wildcardtenant_isolationrule compiled; federated object without the ADR-0090 grandfather stamp), and the stake is fail-open — on SQLite an unresolvable column degrades to a string literal, i.e. the wall silently stops walling. Step one is a bounded seam-tier measurement (a fabricatedExecutionContextwithtenantIdset reachescompileFilterwithout the cloud-private org package), with a fix only if it fires. Not blocked by PR #7833 — that fix is on the objectql layer this card is explicitly outside of.Dedup: not a duplicate of #7737 (boot-order binding, fix merged at
199ec47) or #7738 (engine-layer injection, PR #7833 in flight); this is the plugin-security layer sibling those two do not touch.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
huangyiirene commented
on Aug 11, 2026 CollaboratorMore actionsdomain:identityseat — held in queue behind PR #7792, with the scope pre-recorded. Not shelved; this is a hard predecessor gate with a live ETA.Seat: #6022, session
session_01D6Qi9sYxhaRwj7TYiD5MWg.Why it is held
This card names two injectors. The second one — record-ownership scoping (
__readScopeown/unit, "plugin-security read path") — is not inrls-compiler.ts. Measured onorigin/main:packages/plugins/plugin-security/src/security-plugin.ts:1395 sc.__readScope = this.permissionEvaluator.getEffectiveScope('read', …) packages/plugins/plugin-security/src/security-plugin.ts:2763 ...(readScope ? { __readScope: readScope } : {}),security-plugin.tsis the file PR #7792 (#7665) is currently rewriting — it is armed and in the merge queue as of 19:55Z, and its diff lands +61/−6 in exactly the RLS filter-composition path this card has to reason about (computeLayeredRlsFilter, plus the 2.7 by-id gate and the 2.8controlled_by_parentcheck). Dispatching now would put a dev in the same file as an armed, unmerged PR.Two traps for whoever picks this up, both from #7792
Recorded now because they are worth more before the work starts than after:
- The line numbers in this card's table will be stale. Re-resolve
rls-compiler.ts:119and the two__readScopesites againstorigin/mainafter fix(plugin-security): require by-id write targets to be within the caller's readable set under select-only RLS #7792 lands, by symbol rather than by line. - fix(plugin-security): require by-id write targets to be within the caller's readable set under select-only RLS #7792 changes the answer to part of this card's own question. After it, an empty write-class policy collection derives its scope from the caller's
selectnarrowing. So the set of situations in which "no policy compiles for this object" holds is strictly smaller than it is onmaintoday — which is the premise the wildcard-tenant_isolationhalf of this card rests on. Re-measure the premise on the merged tree; do not carry today's reading across.
⚠️ Flagged as a PM mechanism assumption, to be measured and welcome to be falsified — not a ruling. If the boot in this card's step one shows the derivation does not touch the federated path at all, say so with the measurement and this note is simply wrong.Unchanged by the hold
The card's own fences stand exactly as written: do not widen #7833's engine-level skip; and "latent, no change needed" remains a valid outcome provided it arrives with the measurement rather than as an assumption. The dialect point is the sharpest thing here and survives the wait — a SQLite-only verification proves less than it appears to, in both directions.
Generated by Claude Code
- The line numbers in this card's table will be stale. Re-resolve
huangyiirene commented
on Aug 11, 2026 CollaboratorMore actionsClaim: PM loop round 1 (
domain:identityseat #6022)
Session:session_01D6Qi9sYxhaRwj7TYiD5MWg
Branch:claude/issue-7835-federated-rls-injectors
Worktree:objectstack-issue-7835
Domain:domain:identity
File surface:packages/plugins/plugin-security/src/rls-compiler.ts·packages/plugins/plugin-security/src/security-plugin.ts(the two__readScopesites) + their tests.⚠️ packages/objectql/src/engine.tsis out of surface — this card's own fence forbids widening #7833's engine-level skip. Stop on breach and explain.
Container & model: M (boot-and-measure across two injectors, dialect-dependent),mode:cloud,model: opus
Serial constraints cleared: predecessor gate released — PR #7792 (#7665) merged asb54aaabat ~19:58Z, which is what was holding this card.security-plugin.tsis now free: of this lane's five still-armed PRs, #7796/#7822/#7825 areplugin-auth, #7797/#7806 areplugin-sharing— none touchesplugin-security. No other in-flight claim names either file.Premise re-measured on the merged tree (the two traps from my hold note, now resolved)
1. Line numbers, re-resolved by symbol — one had already drifted, as predicted:
what card said origin/main@b54aaabRLS compiler org injection rls-compiler.ts:119:119— unchanged,organization_id: executionContext?.tenantId__readScopeassignment"plugin-security read path" security-plugin.ts:1395__readScopepropagation— security-plugin.ts:2771(was:2763before #7792 — drifted by 8)2. #7792 did land the narrowing I flagged, and it cuts against injector #1's premise from a second direction — but the bigger finding is independent of it:
The wildcard
tenant_isolationRLS policy has been RETIRED (ADR-0090 D1). Measured onorigin/main:packages/plugins/plugin-security/CHANGELOG.md:450— "Neithermember_defaultnorviewer_readonlycarries atenant_isolation";packages/platform-objects/CHANGELOG.md:133— "D1 retired its wildcardtenant_isolationRLS policy";- every other live hit is a comment or a changelog entry describing the retirement, not a shipped policy.
So this card's own explanation for why the QA capture showed no second predicate — "no wildcard RLS rule had been compiled for that object on that boot" — may not be a property of that fixture at all. It may be that no shipped permission set carries such a rule any more, on any boot. That does not dismiss the card: an app-authored wildcard rule is still expressible, and injector #2 (
__readScopeown/unit) is completely independent of the retirement and remains unmeasured.⚠️ All of the above is a PM mechanism assumption — measure it, and falsifying it is a good outcome. It is not a ruling and it does not pre-decide the verdict.
Generated by Claude Code
huangyiirene commented
on Aug 11, 2026 CollaboratorMore actions{ "issue": 7835, "status": "rework", "branch": "claude/issue-7835-federated-rls-injectors", "pr": "https://github.com/objectstack-ai/objectstack/pull/7859", "premise_still_valid": true, "summary": "Booted and measured; BOTH injectors fire on federated objects, so 'latent, no change needed' is NOT the outcome — but the card's mechanism for injector #1 is falsified. The wildcard `tenant_isolation` RLS policy IS retired (ADR-0095 D1, not ADR-0090 D1), and it was replaced by Layer 0 (`tenant-layer.ts`), which needs no policy to fire: under a walled posture it emits `organization_id` for any object whose field set carries that column. The registry (`applySystemFields`) injects a PHANTOM `organization_id`/`owner_id` into federated objects while `Engine.syncObjectSchema` returns early and issues no DDL for them — so the defect is unconditional, not rule-dependent. Fixed injector #1 in plugin-security by telling Layer 0 the truth about a federated object's tenant anchor, keyed on PROVENANCE (identity against the shipped `TENANT_SCOPE_FIELD_DEF`) rather than `external != null`, so a federated object declaring a real remote org column keeps its wall. Injector #2 (`__readScope` own/unit lowered to an `owner_id` predicate) also measured firing, but its producer is `plugin-sharing/src/sharing-service.ts` — outside this card's declared file surface — so it is reported and filed as #7858 rather than patched. Status is `rework` and the PR reads `Part of #7835` because half the card is deliberately unimplemented, not because the delivered half is incomplete. #7833's engine-level skip was NOT widened and `packages/objectql/src/engine.ts` was not touched (first fence).", "tests": "Build closure first (#6371): `pnpm --filter '@objectstack/dogfood^...' build` green — the first dogfood run of the new pin was RED purely on a stale plugin-security dist, which is the trap that rule exists for. MEASUREMENT (real boot, showcase via @objectstack/verify, origin/main @ b54aaab): registered fields of `showcase_ext_customer` (external, remote table `customers`) = [organization_id, created_at, created_by, updated_at, updated_by, owner_id, owning_business_unit_id, name, email, region, lifetime_value] while the remote table has [name, email, region, lifetime_value]; `security.getReadFilter('showcase_ext_customer', {tenantId:'org_alpha'})` = {organization_id: 'org_alpha'} under `isolated`, {organization_id: {$in: [...]}} under `group` with a membership set, deny sentinel under `group` with none; end-to-end `GET /api/v1/data/showcase_ext_customer` under a walled posture = HTTP 200 with ZERO rows (SQLite). Injector #2 measured on a runtime-registered federated object WITHOUT the ADR-0090 stamp: `sharing.buildReadFilter(..., __readScope='own'|'unit')` = {owner_id: 'usr_member_1'}; the grandfathered `showcase_ext_customer` returns null, exactly as the card predicted. AFTER: `pnpm --filter @objectstack/plugin-security test` 49 files / 996 tests passed; `typecheck` clean; `pnpm --filter @objectstack/dogfood --filter @objectstack/plugin-sharing test` = dogfood 92 passed | 1 skipped (588 tests), plugin-sharing 16 files / 428 tests passed. CONSUMER SWEEP — prefix direction `pnpm --filter '...@objectstack/plugin-security'` (= the 26 packages that DEPEND ON it): build green, typecheck green. GATES: `node scripts/check-nul-bytes.mjs` OK (7180 files); `pnpm check:docs-audit-scope` OK; `pnpm check:engine-double-contract` OK (155 pinned / 133 DEBT / 2 exempt). No new error code, so `check:error-code-casing` is not applicable. REVERSE VERIFICATION — predictions written to a file BEFORE running the mutations, both matched: Ablation A (drop the Layer 0 call-site guard) predicted RED on federated+injected under `isolated` and `group`, RED on both dogfood cases, GREEN on federated+author-declared and on the LOCAL control; actual = exactly that, plus two further reds not itemized in the prediction table ('federated with no active org', 'authored policy still reaches the compiler'), both in the same direction. Ablation B (drop the provenance half so every federated object counts as phantom) predicted exactly one red, 'federated object whose AUTHOR DECLARED a real remote organization_id: the wall STAYS'; actual = exactly one red, that one. GATE STATUS AT REPORT TIME: `in_progress` — only Vercel had reported when the draft PR opened; ESLint and TypeScript Type Check had not started. Not polled, per the dispatch contract.", "open_questions": [ { "question": "Injector #2's producer is `plugin-sharing/src/sharing-service.ts` (`buildReadFilter`/`buildWriteFilter`), not plugin-security — plugin-security only stamps the `__readScope` LABEL, which is a fact about the caller's grant rather than about the storage. The card's table located it in 'plugin-security read path'. Fix it in plugin-sharing, or close #7835 on the tenant half alone?", "options": [ "A — leave as filed: #7858 carries injector #2 with its measurement and a suggested fix; #7835 stays open until that lands, PR #7859 is `Part of`", "B — re-dispatch #7835 with plugin-sharing added to the surface and fold both halves into one PR", "C — close #7835 on the tenant half and let #7858 stand alone" ], "recommendation": "A. The dispatch prompt's own clause is explicit — 'if measurement shows the real producer is elsewhere, report it and stop rather than either patching a consumer or silently widening scope' — and plugin-sharing is a different package with its own suite. B is a defensible follow-up if you would rather have one PR, and is cheap now that the measurement exists: the fix is the same provenance test applied to `OWNER_FIELD_DEF`. C loses the thread that both halves are one defect." }, { "question": "All three fixes to date — the engine's `DriverOptions.tenantId` withhold (#7833), this Layer 0 fix, and the one #7858 proposes — are separate consumers re-deriving 'that column is not really there' about one producer: `applySystemFields` injects platform anchors into objects whose storage the platform never provisions. Should the registry stop injecting them for `external` objects, or mark them so consumers can tell?", "options": [ "A — leave the three consumer-side fixes; the root-cause analysis is recorded in #7858", "B — open a dedicated architectural card for the registry-level answer", "C — do it inside one of the existing cards" ], "recommendation": "B if you want the duplication closed, but not urgently. The blast radius is wide — the `/meta` read path serves the post-injection document (#6562), and Studio forms plus the ADR-0087 declaration-parity ratchet read the same field sets — so it is a real design change, not a cleanup. C is wrong: it would put a wide objectql change inside a card fenced to plugin-security. A is acceptable if a fourth consumer never appears, which I would not bet on." }, { "question": "No Postgres/MySQL run was performed — the container has SQLite only, and SQLite is precisely the dialect where the pre-fix defect is invisible (constant-false string comparison, HTTP 200, 0 rows) rather than an error. Is the dialect evidence sufficient?", "options": [ "A — accept: every filter-shape assertion is dialect-free by construction, asserting the FilterCondition plugin-security composes before any driver sees it; only the one end-to-end case is SQLite-bound", "B — require a Postgres re-verification before the ready-flip" ], "recommendation": "A. The card's warning is that a SQLite-only verification proves little — which is exactly why the load-bearing pins assert the composed predicate rather than a row count. The SQLite end-to-end case is reported as evidence for the SQLite-shaped symptom only, and the PR says so explicitly." } ], "out_of_scope_findings": [ "filed as #7858: plugin-sharing lowers `__readScope` own/unit to an `owner_id` predicate on federated objects, where `owner_id` is a phantom injected column — measured firing on a federated object without the ADR-0090 grandfather stamp; the body also records the shared `applySystemFields` root cause", "not filed (already named in #7835's own body, driver lane): `SqlDriver.computeTenantField` still resolves `organization_id` for a federated object and would act on an explicitly-passed tenantId; unchanged by this PR" ] }
Generated by Claude Code
Generated by Claude Code
huangyiirene commented
on Aug 11, 2026 CollaboratorMore actionsPM review: ACCEPT the tenant half (PR #7859).
domain:identityseat #6022, sessionsession_01D6Qi9sYxhaRwj7TYiD5MWg. Card stays open — correctly — because the ownership half moved to #7858.First: my dispatch assumption was wrong, and wrong in the direction that mattered
I told this dev that the wildcard
tenant_isolationpolicy looked retired (citing ADR-0090 D1 from changelog prose) and flagged it as something that might make injector #1 latent. Both halves were off:- the retirement is ADR-0095 D1, not ADR-0090 D1;
- and far more importantly, retiring the policy did not remove the wall — it moved it to Layer 0 (
tenant-layer.ts), which fires with no policy at all. So the retirement does not make the defect conditional or latent. It makes it unconditional: every federated read under a walled posture gets the phantom predicate, policy or no policy.
I reasoned from changelog prose and reached a conclusion that pointed away from the defect. The dev booted it and reasoned from the running system. That is the correction working as designed, and it is on the record here rather than only in the thread. Anyone reading my claim comment above should read this paragraph with it.
What I verified independently (not from the report)
check result Path face — docs/adr/**/.claude/skills/**/skills/**✅ zero hits; normal terminal path First fence — packages/objectql/src/engine.tsuntouched✅ 0 occurrences in the diff First line is Part of #7835, notFixes✅ correct — a Fixeshere would have silently closed a card that is half-doneProvenance test is real, not external != null✅ federated-phantom-anchors.ts:133compares againstTENANT_SCOPE_FIELD_DEFidentity; theexternal != nullat :118 is a separate conjunctThe one undeclared out-of-surface change ✅ legitimate — @objectstack/metadata-coremoveddevDependencies → dependenciesbecause runtime code now importsTENANT_SCOPE_FIELD_DEF. Correct, not scope creep.One cosmetic nit, not blocking: the same
package.jsonedit re-encoded thedescriptionem dash as—. Semantically identical JSON; just an unrelated diff line.The strongest thing in this PR is Ablation B. Dropping the provenance half turned exactly one pin red — the federated object whose author declared a real remote
organization_idkeeps its wall. That is what stops the provenance test from being decoration, and it is the difference between this fix and the naiveexternal != nullversion that would have silently deleted a working tenant wall. The dev also noted that Ablation A produced two reds its prediction table had not itemized, and reported them rather than back-filling the table — which is the honest direction.The three open questions — all answered here, none escalated
Q1 — injector #2's producer is
plugin-sharing. → Option A, as recommended. My dispatch said "if measurement shows the real producer is elsewhere, report it and stop rather than patching a consumer or silently widening scope", and the dev did exactly that. Note for the record:plugin-sharingis insidedomain:identity, so #7858 is this same seat's card — it will be claimed and dispatched from here, not transferred. Not folded into this PR (option B) because the tenant half is finished and verified; re-opening it to append a second package would put two review surfaces in one PR for no gain.Q3 — dialect coverage. → Option A, accepted. I set the dialect requirement in the dispatch, and this is a good answer to it rather than a dodge: the load-bearing pins assert the composed
FilterCondition, which is built before any driver sees it and is therefore dialect-free by construction. Only the end-to-end case is SQLite-bound, and the PR says so in its own words instead of letting a SQLite green imply more than it proves. No Postgres re-run required.Q2 — the registry-level root cause. → Option B, and I am filing it. Three separate consumers (#7833's engine fix, this one, #7858's proposal) have now independently re-derived "that column is not really there" about one producer,
applySystemFields. Three is enough to stop calling it a coincidence. It lands in the registry rather than in this lane, so it goes to the backlog for triage to route and grade — ⛔ I am not pre-grading it, and ⛔ not folding it into any of the three cards (the dev's option C, correctly rejected: a wide registry change fenced inside a plugin-security card).Status field
The report says
rework; I am recording ACCEPT instead. Nothing about the delivered half needs rework — the dev usedreworkto signal "half the card is deliberately unimplemented", which is whatPart of+ #7858 already express. Flagging the wording only so the label is not read later as a quality verdict on this PR.Next: CI convergence, then ready-flip and queue. ⛔ The dev does not poll CI and does not arm the queue — that is this seat's job.
Generated by Claude Code
- added a commit that references this issue
on Aug 11, 2026 huangyiirene commented
on Aug 11, 2026 CollaboratorMore actions{ "issue": 7835, "status": "done", "branch": "claude/issue-7835-federated-rls-injectors", "pr": "https://github.com/objectstack-ai/objectstack/pull/7859", "premise_still_valid": true, "summary": "Narrow CI-convergence patch round on the already-ACCEPTed PR #7859 — no analysis redone, the fix untouched, no new PR. The only red job was ESLint: `slot-lookup/no-any-assignment` on the two service lookups in `packages/qa/dogfood/test/federated-rls-injectors.dogfood.test.ts`, inert locally but live on the runner because CI runs `eslint . --no-inline-config`. Took the rule's own prescription — stopped erasing the lookups and passed each slot's real contract: `ql` is `IObjectQLEngine` and `security` is `ISecurityService`, both from `packages/spec/src/contracts/core-service-contracts.ts` (found by reading the non-test call sites, e.g. `packages/rest/src/rest-api-plugin.ts:234` and `:324`, `packages/plugins/plugin-security/src/security-plugin.ts:646`). `ObjectKernel.getService` is already generic over the slot (`packages/core/src/kernel.ts:532`), so the `stack.kernel as any` handle bought nothing and is gone too. No assertion changed; all four cases still pass. What the honest typing surfaced: `IObjectQLEngine.getSchema` declares `unknown` — so the PREMISE case's `schema.external` read was never type-checked at all under the old `any`. That is deliberate contract looseness, not a defect: the engine's own return is `ServiceObject | undefined` (`packages/objectql/src/engine.ts:4766`), and the contract's header keeps its edges loose so `spec` never depends on the engine package, explicitly telling consumers to narrow at the call site. So the case narrows to `ServiceObject` — a named spec type `@objectstack/dogfood` already depends on — rather than re-erasing to `any`. Nothing else objected.", "tests": "Build closure FIRST (stale-dist trap): `pnpm --filter '@objectstack/dogfood^...' build` -> green (`examples/app-showcase build: Done`). BEFORE: `npx eslint <file> --no-inline-config` -> `83:5 error` + `84:5 error slot-lookup/no-any-assignment`, `2 problems`, reproducing CI job 93928133743 exactly. AFTER: same command -> ESLINT_EXIT=0, no output. `pnpm --filter @objectstack/dogfood typecheck` (tsc --noEmit) -> green. `pnpm --filter @objectstack/dogfood test federated-rls-injectors` -> `Test Files 1 passed (1) / Tests 4 passed (4)`, 13.50s. `pnpm check:slot-lookup` -> `ratchet holds: 108 unswept site(s) in 25 file(s), none new. baseline key set verified against b54aaab: no files added` (i.e. this file was NOT parked in the baseline). `node scripts/check-nul-bytes.mjs` -> `OK (scanned 7180 text file(s) ... no raw ASCII control bytes)`. REVERSE VERIFICATION, both directions, predicted RED before running — because 'the honest types compiled' proves nothing unless tsc actually reads the file (the PINS_CHECKED / phantom-check trap): (1) drop the `ServiceObject` narrowing -> `test/federated-rls-injectors.dogfood.test.ts(101,20): error TS2339: Property 'external' does not exist on type '{}'`; (2) misspell the security call as `getReadFilterNope` -> `(109,35): error TS2551: Property 'getReadFilterNope' does not exist on type 'ISecurityService'. Did you mean 'getReadFilter'?`. Both red, so both contract types are live and enforced rather than silently inferred as `any`. Fix taken out and restored via a scratchpad file copy, never `git stash`. Pushed to the same branch, no force-push, no rebase: e0d3160..228a2f0, remote tip confirmed 228a2f0799a67d59d7935303efa921c51fcbbaf8.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsClosing — both halves are accounted for, and leaving this card
pm:dispatchedwith no live dev is a half-state. Seat #6022, sessionsession_01D6Qi9sYxhaRwj7TYiD5MWg.injector outcome ① tenant — Layer 0 emitting a phantom organization_idon federated objectsFIXED and MERGED — PR #7859, verified on origin/main(federated-phantom-anchors.tspresent, provenance test againstTENANT_SCOPE_FIELD_DEFintact)② ownership — __readScopeown/unit lowered to anowner_idpredicate on the same phantom column#7858, pm:queuein this lane, with the measurement already takenPR #7859 was correctly opened as
Part of #7861— sorry,Part of #7835— precisely because half was deliberately unimplemented. That half now has its own card with its own measured evidence, so this card tracks nothing #7858 does not. Keeping it open would leave a card that reads dispatchable but has no dispatchable work in it.What this card produced, for the record:
- The measured refutation of its own mechanism claim: the wildcard
tenant_isolationpolicy is retired (ADR-0095 D1, not ADR-0090 D1 as I guessed in my dispatch), and the retirement moved the wall to Layer 0, which needs no policy to fire. So the defect was never rule-dependent and never latent — it was unconditional on every federated read under a walled posture. My dispatch assumption pointed away from the defect; the dev booted it and corrected me, and that correction is on this card above. - The fix keyed on provenance (identity against the shipped
TENANT_SCOPE_FIELD_DEF) rather than the naiveexternal != null, so a federated object that declares a real remote org column keeps its wall. Ablation proved that distinction load-bearing: dropping the provenance half turned exactly one pin red, the author-declared case. - Two follow-ups filed: plugin-sharing lowers
__readScopeown/unit to anowner_idpredicate on federated objects, whereowner_idis a phantom column #7858 (injector ②) and [Decision]applySystemFieldsinjects platform anchors intoexternalobjects the platform provisions no storage for — three consumers have now independently re-derived "that column is not really there" #7865 (the registry root cause — three consumers have now independently re-derived "that column is not really there" aboutapplySystemFields). - One process finding: [finding]
rethrowAsBetterAuthErrormaps engine errors code-by-code, so each new engine code needs its own incident before it stops leaking as a bodyless 500 #7881, the code-by-code error mapping that makes each new engine code need its own incident.
Closing as completed. ⛔ Not reopening for injector ② — that is #7858's job and it is already queued.
Generated by Claude Code
- The measured refutation of its own mechanism claim: the wildcard
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 6, 2026 - added a commit that references this issue
on Sep 9, 2026
Reported by #7738's dev as
out_of_scope_findings(report §4), deliberately not fixed there —plugin-securitywas outside that card's cleared lane. Filed unassigned.Context: what #7738 did and did not close
#7738 / PR #7833 withholds
DriverOptions.tenantId(and thegroup-posturetenantIdsunion) for objects withexternal != null, atpackages/objectql/src/engine.ts:2528. Withinpackages/objectqlthat is the complete set — the dev measured that the engine injects noowner_id,owning_business_unit_id,created_byorupdated_byread predicate anywhere, soorganization_idwas the whole problem in that lane.Two sibling injectors live on a different layer and are untouched.
The two siblings
current_user.organization_idfromexecutionContext.tenantIdand folds it into the query ASTorganization_idpackages/plugins/plugin-security/src/rls-compiler.ts:119__readScopeown/unit)owner_id-shapedA compiled wildcard
tenant_isolationrule would add a second, independent org predicate to a federated read — one that #7833's engine-level fix does not reach, because it is applied at a different layer.This is a hypothesis with a named place to check it, not an observed defect. #7738's QA capture did not show either predicate, for two identifiable reasons that are properties of that fixture rather than of the code:
showcase_ext_customercarries the ADR-0090 D1 grandfather stampsharingModel: 'public_read_write', and the run recorded__readScope: 'org'. A federated object without that stamp would take the same class of defect through the secure-default OWD.Reporter's own stated evidence strength: read from source plus the QA capture, not booted to confirm. Step one is to boot it — with a wildcard
tenant_isolationrule compiled, and separately with a federated object that lacks the grandfather stamp.Why it matters as much as #7738 did
The failure is fail-open, and that is what makes it worse than an error. On SQLite, an identifier that cannot resolve to a column is reinterpreted as a string literal, so
('organization_id' = 'org_…' or 'organization_id' is null)goes constant-false on both disjuncts: 0 rows, no error, HTTP 200. Postgres/MySQL would raisecolumn "organization_id" does not existinstead. The symptom is dialect-dependent; the defect is not. A fix — or a clean bill of health — verified only on SQLite proves less than it appears to.⛔ Fences
Also noted by the same report, same family, different lane
SqlDriver.computeTenantField(sql-driver.ts:5663) still resolvesorganization_idfor a federated object; after #7833 it simply never receives atenantIdto act on. A driver-sideexternalbranch would be defence in depth against a future caller that passestenantIdexplicitly. That is the driver lane — mentioned so it is not lost, not folded in here.Related