Repository navigation
Description
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 命令)
- P1
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)。 - P2 driver-sql 的自动限定不读姿态:
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。 - P3 内存驱动忽略
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)。 - P4 实测表:service-analytics: on driver-sql (sqlite) the NativeSQLStrategy answers
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 路径不加这条谓词,读全量。
选项 × 真实代价
| 选项 | 做什么 | 客户可感知后果(实测/推断) |
|---|---|---|
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 = 不同柜台各自发挥。
四轴
- ① 实际业务需求:有实测拉动——ats 演示应用在默认驱动上管理员读 0 行(service-analytics: on driver-sql (sqlite) the NativeSQLStrategy answers
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)。这是「今天谁撞上」的直接答案。 - ② 项目长远合理性:A 让「姿态」成为租户墙的唯一开关(ADR-0105 的本意),少一处平行判定;B 把驱动层的一个实现细节抬成协议,并要求其他驱动复制它,与 [finding] The InMemory driver silently ignores the engine's tenant scope for objects that OMIT a
tenancyblock — its guard only refuses an explicitenabled: true, so memory-driver runs show cross-organization rows a SQL driver refuses #16589 裁决冲突。 - ③ 防 AI 犯错:A 后错误形态是「多看见」但只在声明为单租户的部署里,且响亮(同一部署所有路径一致);今天的形态是静默——两个路径悄悄给不同数字,AI 写的仪表盘会把 0 当真。
- ④ 创业阶段不扩散:A 不加配置、不加姿态、不加驱动能力,删一条多余判定;B 要在两个驱动和一个服务里各加一段墙。
推荐 + 回退 + 置信缺口
推荐 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 记录已知分歧。
- ① 项目长远合理性:A 缩小特例——姿态成为租户墙唯一开关,删一处平行判定。
- ② 实际业务拉动:今天撞上——ats 演示应用管理员在 sqlite 上读 0 行(service-analytics: on driver-sql (sqlite) the NativeSQLStrategy answers
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)。 - ③ 防 AI 犯错:今天两条路径静默给出不同数字;A 后同一部署所有路径一致、响亮。
- ④ 创业阶段不扩散:A 不加配置不加能力;B 要在两驱动一服务复制墙且与 [finding] The InMemory driver silently ignores the engine's tenant scope for objects that OMIT a
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与姿态的关系未逐对象核过。
Activity
huangyiirene commentedon Sep 9, 2026
Analysis correction — this question is already ruled by ADR-0131 D8; the card's
Governing text:omitted it (director seat, summon #18,session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-09T02:3xZ)⛔ Not a ruling. This supersedes the recommendation this seat presented in batch #104 (chat, 02:0xZ), which was built on the card's own governing-text list and is withdrawn.
What the corpus says, read this summon on
origin/main8d4690b8:single= one logical tenant; the platform bootstraps a Default Organization so the admin session carries anactiveOrganizationId(plugin-auth/src/ensure-default-organization.ts). ADR-0095 D1 and ADR-0105: undersingleLayer 0 is inert.= :tenantunderisolated,IN accessible_org_idsundergroup, nothing undersingle— and threads the same value to Layer 0 and to every driver. No layer has a NULL arm.」 D9: undersinglethe Default Organization is the only organization and the derived owner. D11: 「Postures differ only in enforcement …singleis never a reason for a row to be org-less.」organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212 (C8, protocol 18) — 「The engine computes one tenant scope —= :tenant/IN accessible_org_ids/ none undersingle— and threads it tocomputeTenantLayer0Filterand toDriverOptions;applyTenantScopedrops bothorWhereNullarms」 —pm:blockedbehind [gate] the v18 development line is not open — ADR-0131 execution cards are blocked on this card #15193, the v18 gate: 「⛔ Nothing else of ADR-0131 ships in 17.x. A card that would land "just the harmless half" of any of D1–D13 in a 17.x release is out of order regardless of this gate.」 Maintainer, 2026-09-04: 「要 v18 才开发」.⇒ Option A of this card is D8's
singlebranch. It is ruled, scheduled and gated; it is not an open question, and landing it in 17.x is barred by the staging ruling unless the maintainer reopens that.Why the two walls disagree today (the card's mechanism is right; the framing is not):
applyTenantScopefires on anyoptions.tenantIdand cannot see the posture (its own docblock says so);buildDriverOptionsthreadstenantIdwhenever the context carries one;computeTenantLayer0Verdictanswersnoneundersingle. That is the pre-D8 state ADR-0131 §1 describes, not a new defect.Why the ats deployment reads 0 rows (measured in the
objectstack-ai/atstree,446ab44; ats#39 itself not readable from this session):DESIGN.md§03 「雇主 = 平台组织」 — the demo seeds onesys_organizationper employer and stampsorganization_id: orgId(employer)on the four walled objects (src/data/shared/build.ts), on a deployment declaredsingle. The platform'ssingleis one organization; the admin's active organization is the Default Organization; the driver arm hides every employer-stamped row. The same shape is refused on the write side by #8844'sambiguous-organization(atsDESIGN.mdline 303: 「本安装声明singleposture 却持有 2 个组织」, ats#56 / #16659). ats's own design says the split is built for a walled posture and was waiting on #16215 (merged 2026-09-07) and #16137 (closed 2026-09-08, PR #16719) — both landed, so an open-only install can raise the wall today.Options as they actually stand (B and C are excluded by D8 and by the #16589 ruling 5580215080):
pm:blocked,Blocked-by: #15193; feat(spec,drivers,objectql,plugin-security):organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212's acceptance gains one line (undersingle,/data, analytics and the memory driver answer the same user the same numbers — service-analytics: on driver-sql (sqlite) the NativeSQLStrategy answersPOST /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's table as the fixture); 17.x engine and driver untouched; ats moves to a walled posture (its side, ats#39).singlebranch (engine withholdstenantIdundersingle); reverses the 2026-09-04 staging ruling;Clause-②: yes, minor + BREAKING; leaves the driver's NULL arm in 17.x beside a wall-lesssingle.Recorded for the process: this seat presented the original A/B/C frame without running the director charter's pre-presentation corpus grep (
references/lanes/director.mdline 45); the card as filed listed ADR-0105 / ADR-0021 and not ADR-0131. Filed as a skills-lane finding.维护者速读(更正稿)
事情:「
single姿态下哪道租户墙是平台的」已经由 ADR-0131 D8 定了——引擎按姿态算一次谓词,single下不算,同一个值给安全层和每个驱动;驱动自己的 NULL 臂退役。执行卡 #15212 排在 v18,被闸门 #15193 挡着,你 09-04 裁「要 v18 才开发」。今天 sqlite 上管理员读 0 行,是 ats 演示应用把「雇主 = 组织」建在single姿态上——平台的single只有一个组织;ats 自己写明这套设计是给带墙姿态准备的,它等的两张卡都已落地。选项:甲(推荐) 本卡不另裁,转为阻塞在 #15193 后随 #15212 执行;ats 切到带墙姿态。乙 你明示在 17.x 开例外,只做「
single下引擎不给驱动下发租户限定」这一半——要推翻 09-04 的分期裁决。你要做的:回一个字,甲 或 乙。
Generated by Claude Code
os-bill commentedon Sep 9, 2026
Ruling recorded — CLOSE (director seat, summon #20, decision batch #105 item 3, 2026-09-09T04:3xZ)
Provenance (who / verbatim / where): maintainer, live PM chat with this seat (
session_01Tep4AYXZvyBA7jsvne5KZV, GitHubos-bill), 2026-09-09T04:3xZ, replying to batch #105 in which this card was item 3 presented as 甲 / 乙 (summon #18's corrected frame, 5594769261) with the recommendation 甲. Reply, verbatim: 「16934 关闭 ;其他同意」.Ruled. This card closes. Neither 甲 (park it as
pm:blockedbehind #15193) nor 乙 (a 17.x exception landing D8'ssinglebranch alone) is taken: no new ruling on the substance, no 17.x exception. The question the card asks is already answered by ADR-0131 D8 (the engine computes the tenant read scope from posture and context — nothing undersingle— and threads the same value to Layer 0 and to every driver), with execution on #15212,pm:blockedbehind the v18 gate #15193 (maintainer 2026-09-04: 「要 v18 才开发」). The ats reading (platform admin 0 rows on sqlite) is the ats deployment's posture choice — one organization per employer on a deployment declaredsingle— and is that app's to move (ats#39); it is not a 17.x platform defect.State, one write: closed
not_planned;needs-user-decisionremoved;priority:p1 · security · domain:enginekept. Nothing on #15212, #15193 or #16645 moves by this ruling. Reopening is free if the maintainer wants 乙 later.Generated by Claude Code
feat(pm): mechanise the prior-ruling search for decision cards (objec…
fix(pm): check-prior-rulings parses the `## Decision` shape and names…