Split out of #16645 (condition 5 of its triage rubric), which closes when PR #16860 merges and would otherwise take this open question with it. The dev seat on #16860 escalated it as needs_decision; the PM seat concurred; no ruling exists. Filed by the director seat at 17:1xZ, 2026-09-08.
Governing text: ADR-0105 (tenancy postures) · ADR-0021 D-C (getReadScope threading) · plugin-security computeTenantLayer0Verdict (postureEnforcesWall('single') === false, boot log 「tenancy posture 'single' — Layer 0 is inert」) · Engine.buildDriverOptions (DriverOptions.tenantId set whenever execCtx.tenantId !== undefined && !isTenancyDisabled(schema) && !isFederated — reads no posture) · maintainer ruling 2026-09-08 on #16589, verbatim 「16589 内存驱动不需要支持多租户,业务上没有任何意义啊」.
一句话问题
同一个 single 姿态的部署里,平台管理员在 sqlite 上走 /data 读到的是 0 条,走 analytics 或换成内存驱动读到的是 12 / 30 / 40 / 14 条——两道「租户墙」各说各话,客户看到的是「同一个人、同一份数据,换个接口或换个驱动,数就变了」。
前提(带 re-check 命令)
具体问题
single 姿态是「一个部署一个组织」。今天 driver-sql 仍按 execCtx.tenantId 追加 (organization_id = :tenant OR organization_id IS NULL),而 Layer 0 按姿态判定「无墙」。平台管理员(viewAllRecords)的 tenantId 与种子数据的 organization_id 不一致时,sqlite 读 0 行;内存驱动与 analytics 原生 SQL 路径不加这条谓词,读全量。
选项 × 真实代价
| 选项 |
做什么 |
客户可感知后果(实测/推断) |
A(推荐) 姿态是唯一权威:single ⇒ 引擎不给驱动下发 tenantId(buildDriverOptions 读姿态;或 Layer 0 none 时清空 tenantId) |
sqlite /data 从 0 变回 12/30/40/14,与内存驱动、analytics 一致;group / isolated 姿态不变(墙照旧)。推断:单租户部署里「管理员看不到自己数据」(ats#39)消失。代价:single 部署里带脏 organization_id 的行全部可见——本就该可见 |
|
| B driver-sql 的自动限定是平台墙:其他路径(内存驱动、analytics 原生 SQL)补上同一谓词 |
与 #16589 裁决相悖(内存驱动不做多租户);analytics 补墙会把求职者的 RLS 限定 9 也归零(#16860 验收备注第 5 条实测);single 部署里管理员继续读 0 |
|
| C 各走各的,只记档 |
同一部署两种答案成为长期状态;每个新读路径都要再选一次 |
|
业务含义直译:A = 一个店铺的老板能看见自己店里所有货;B = 老板要先证明每件货是他的,证不出来就当没有;C = 不同柜台各自发挥。
四轴
推荐 + 回退 + 置信缺口
推荐 A。回退 C(暂不动,只在 #16645 关闭后保留本卡)。置信缺口:未在真实多组织(group / isolated)部署上测 A 是否影响墙——A 只改 single 分支,但 buildDriverOptions 的读姿态位置需要 dev 实测确认不误伤;isTenancyDisabled(schema) 与姿态的关系未逐对象核过。
裁后执行
A ⇒ domain:engine 卡:buildDriverOptions(或 Layer 0 verdict 处)在 single 姿态下不下发 tenantId,两驱动等价 pin(sqlite 与内存在 single 下同数;group 下 sqlite 仍加墙),Clause-②: yes(读路径接受集变化),changeset minor + BREAKING(single 部署可见行集变宽)。B ⇒ 先撤 #16589 裁决再立卡。C ⇒ 关本卡 not planned 并在 ADR-0105 记录已知分歧。
Split out of #16645 (condition 5 of its triage rubric), which closes when PR #16860 merges and would otherwise take this open question with it. The dev seat on #16860 escalated it as
needs_decision; the PM seat concurred; no ruling exists. Filed by the director seat at 17:1xZ, 2026-09-08.Governing text:ADR-0105 (tenancy postures) · ADR-0021 D-C (getReadScopethreading) ·plugin-securitycomputeTenantLayer0Verdict(postureEnforcesWall('single') === false, boot log 「tenancy posture 'single' — Layer 0 is inert」) ·Engine.buildDriverOptions(DriverOptions.tenantIdset wheneverexecCtx.tenantId !== undefined && !isTenancyDisabled(schema) && !isFederated— reads no posture) · maintainer ruling 2026-09-08 on #16589, verbatim 「16589 内存驱动不需要支持多租户,业务上没有任何意义啊」.一句话问题
同一个
single姿态的部署里,平台管理员在 sqlite 上走/data读到的是 0 条,走 analytics 或换成内存驱动读到的是 12 / 30 / 40 / 14 条——两道「租户墙」各说各话,客户看到的是「同一个人、同一份数据,换个接口或换个驱动,数就变了」。前提(带 re-check 命令)
single姿态下 plugin-security 的 Layer 0 不生效:git grep -n "postureEnforcesWall" packages/plugins/plugin-security/src | head(阳性对照:git grep -c "computeTenantLayer0Verdict" packages/plugins/plugin-security/src≥ 1)。git grep -n "tenantId" packages/objectql/src/engine.ts | grep -i "buildDriverOptions\|isTenancyDisabled";git grep -n "organization_id IS NULL" packages/drivers/driver-sql/src | head。tenantId([finding] The InMemory driver silently ignores the engine's tenant scope for objects that OMIT atenancyblock — its guard only refuses an explicitenabled: true, so memory-driver runs show cross-organization rows a SQL driver refuses #16589,维护者已裁不需支持):git grep -n "tenantId" packages/drivers/driver-memory/src | head(零命中 + 阳性对照git grep -c "find(" packages/drivers/driver-memory/src≥ 1)。POST /analytics/dataset/querywithout the object-level grant or the tenant wall — a user with NO read grant on an object gets its row count (200) whereGET /data/OBJECTanswers 403; the memory driver's path refuses both #16645 正文(ats 应用,demo seed,两驱动)。具体问题
single姿态是「一个部署一个组织」。今天 driver-sql 仍按execCtx.tenantId追加(organization_id = :tenant OR organization_id IS NULL),而 Layer 0 按姿态判定「无墙」。平台管理员(viewAllRecords)的tenantId与种子数据的organization_id不一致时,sqlite 读 0 行;内存驱动与 analytics 原生 SQL 路径不加这条谓词,读全量。选项 × 真实代价
single⇒ 引擎不给驱动下发tenantId(buildDriverOptions读姿态;或 Layer 0none时清空tenantId)/data从 0 变回 12/30/40/14,与内存驱动、analytics 一致;group/isolated姿态不变(墙照旧)。推断:单租户部署里「管理员看不到自己数据」(ats#39)消失。代价:single部署里带脏organization_id的行全部可见——本就该可见single部署里管理员继续读 0业务含义直译:A = 一个店铺的老板能看见自己店里所有货;B = 老板要先证明每件货是他的,证不出来就当没有;C = 不同柜台各自发挥。
四轴
POST /analytics/dataset/querywithout the object-level grant or the tenant wall — a user with NO read grant on an object gets its row count (200) whereGET /data/OBJECTanswers 403; the memory driver's path refuses both #16645 表、ats#39)。这是「今天谁撞上」的直接答案。tenancyblock — its guard only refuses an explicitenabled: true, so memory-driver runs show cross-organization rows a SQL driver refuses #16589 裁决冲突。推荐 + 回退 + 置信缺口
推荐 A。回退 C(暂不动,只在 #16645 关闭后保留本卡)。置信缺口:未在真实多组织(
group/isolated)部署上测 A 是否影响墙——A 只改single分支,但buildDriverOptions的读姿态位置需要 dev 实测确认不误伤;isTenancyDisabled(schema)与姿态的关系未逐对象核过。裁后执行
A ⇒
domain:engine卡:buildDriverOptions(或 Layer 0 verdict 处)在single姿态下不下发tenantId,两驱动等价 pin(sqlite 与内存在single下同数;group下 sqlite 仍加墙),Clause-②: yes(读路径接受集变化),changesetminor+ BREAKING(single部署可见行集变宽)。B ⇒ 先撤 #16589 裁决再立卡。C ⇒ 关本卡 not planned 并在 ADR-0105 记录已知分歧。POST /analytics/dataset/querywithout the object-level grant or the tenant wall — a user with NO read grant on an object gets its row count (200) whereGET /data/OBJECTanswers 403; the memory driver's path refuses both #16645、ats#39)。tenancyblock — its guard only refuses an explicitenabled: true, so memory-driver runs show cross-organization rows a SQL driver refuses #16589 相悖。推荐 A(回退 C)。置信缺口:A 对
group/isolated姿态的无损性需 dev 实测,isTenancyDisabled与姿态的关系未逐对象核过。