Skip to content

On driver-memory a time-triggered flow that touches per-organization data has NO legal configuration — PR #17334 moves it from the served case into the refused one #17446

Description

@os-bill

Filed by the domain:spec execution seat (session_01MkQhmuuJAVDjmeWNixwDDH), 2026-09-10T14:52Z, out of PR #17334 (card #16659). ⛔ Unlabelled and ungraded — routing, grading and type are triage's.

⚠️ Read the disposition section FIRST. A dedup search found a standing maintainer ruling that almost certainly decides this card without anyone re-deciding anything, and it is the reason this was not filed into the decision box.

The measurement

The at-tier review of PR #17334 found the changeset banner's claim — that a single-organization install "loses nothing at run time" — false on @objectstack/driver-memory. The implementing round verified it by running the driver's own exported predicate, assertCallNotTenantScoped (packages/drivers/driver-memory/src/memory-tenancy-guard.ts:303), over all twelve doors it guards:

the call the engine hands the driver result
scopedtenantId present (the shape a DECLARED flow produces) 12 of 12 REFUSED, MEMORY_MULTI_TENANT_UNSUPPORTED
unscoped — no tenantId (the shape a job tick produced before the change) 0 of 12 refused, 12 served

The guard has no organization-count limb at all — its only early return is if (tenantId === undefined && tenantIds.length === 0) return; — so a single-organization install is not exempt. Engine side confirmed at packages/objectql/src/engine.ts:4058: hasTenant = execCtx.tenantId !== undefined && !isTenancyDisabled(objectSchema) && !isFederated, then opts.tenantId = execCtx.tenantId.

⭐ Why this is a functional gap and not a documentation defect

On driver-memory, a time-triggered flow that touches genuinely per-organization data has no legal configuration:

The one configuration that is served — declaring the swept objects platform-global (tenancy: { enabled: false }, ADR-0066) — is exactly what the guard's own refusal message says must not be reached for on data that really is per-organization.

⚠️PR #17334 does not create this limitation — the driver has never had row-level tenant isolation (#16589). What it does is move a time-triggered flow from the served case into the refused one. That is the honest framing and it is the whole claim here.

⛔ PR #17334 correctly stopped at this boundary: it fixed the false sentence (the banner is now driver-conditional and names the refusal) and named the gap in the migration entry's acceptance criteria, without touching the gap itself. ⭐ Reported rather than widened — the right call, and the reason this card exists.

⭐ Disposition — the maintainer has already ruled on this family

#5499 — 「[裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点)」 — is a standing cross-card anchor, and it speaks to this directly:

driver-memory 是早期为测试便利设计的组件,方向已定为 sqlite 内存模式替代

and it sets a standing triage rule for exactly this situation:

新单分诊规则(常设):driver-memory / driver-mongodb 族新单照常按落点打 domain:engine,但直接挂 pm:on-hold 引用本单,不入 pm:queue

⇒ ⛔ This is not a decision-box card and I did not file it as one. The implementing round offered the maintainer three options (A: declare driver-memory unsupported for time-triggered data flows · B: an explicit exemption serving such runs unscoped with a loud warning · C: narrow the guard to installs holding more than one sys_organization) and recommended A. ⭐ #5499 already IS option A, for the whole component — the direction is replacement by sqlite :memory:, not extension. Putting this in the decision box would ask the maintainer to re-decide something decided five weeks ago.

⚠️ I also checked the ruling's exception escalation channel, because it would override the freeze:

例外升级通道:若 driver-memory 的缺陷影响 CI 判绿的正确性(单测后端语义错造成测试假绿/假红),按 restore-invariant 处理

It does not apply. PR #17334's own dogfood test (packages/qa/dogfood/test/schedule-sweep-organization-scope.dogfood.test.ts) runs the memory leg and asserts the refusal, with a paired control proving the refusal is about the scope and not about the fixture. So the semantics are pinned, not silently wrong: no false green, no false red. ⛔ No escalation is owed.

Suggested (⛔ not applied — triage's act): domain:engine, pm:on-hold referencing #5499, per that ruling's standing rule. If triage disagrees and thinks the flow-kind × driver support matrix is a separate axis from the component freeze, then it belongs in the box — but that is triage's call to make, not this seat's.

Restart condition, if it is held

#5499's own: when the sqlite :memory: replacement is complete on the test surface, the cards hung under it are closed not planned as a batch. ⭐ That would dispose of this one correctly too — a flow kind that cannot be served by a component being retired needs no fix, only the retirement.

⚠️ One thing worth carrying to whoever closes that batch: this card is not a driver-memory defect. It is a statement about which flow kinds a driver can serve. If the sqlite :memory: replacement lands with the same absence of row-level tenant isolation, the gap moves with it rather than closing — so the batch close should check the successor's guard, not just the component's removal.

Re-check

git -C /home/user/objectstack show origin/main:packages/drivers/driver-memory/src/memory-tenancy-guard.ts | grep -n 'tenantIds.length === 0'
git -C /home/user/objectstack show origin/main:packages/objectql/src/engine.ts | sed -n '4050,4062p'

⛔ Re-run the twelve-door probe through the driver's own exported assertCallNotTenantScoped rather than re-implementing the predicate, and keep the unscoped leg as the control — without it, "12 refused" does not distinguish a tenancy guard from a broken fixture.

Refs: PR #17334 · card #16659 · the at-tier review 5619271717 (M2) · the implementing round's report 5620579622 · #5499 (the governing ruling) · #16589 (the driver's absent isolation) · ADR-0066 (tenancy.enabled: false)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions