Repository navigation
Description
PR #15687 merged with the mislabelling path its own contract review said must not merge — and the code says so in its own docblock.
What is on main right now
packages/objectql/src/engine.ts, in the producer's own comment (added by that PR):
⛔ What is NOT mirrored: clause ③ is deployment-declared and invisible to the engine — no
platformGlobalObjectsreading exists here — so a deployment-exempted object under an armed wall is still stamped with the caller's organization while Layer 0 composed no wall. That population is the seam #15706 rules on, and the reason this producer's PR is Blocked-by it.
bulkEventOrganizationId is conservative on seven axes and then, for isolated, ends return eventOrganizationValue(execCtx.tenantId). Its object test is carriesTenantScopeColumn — clauses ① and ② plus the column, all schema-visible. Clause ③ is not among them. Re-verified: deploymentOrgScopingEntitlement in engine.ts = 0; the two platformGlobalObjects hits are that comment, not a reading.
⇒ On a deployment-exempted object under isolated, a bulk batch that Layer 0 did not confine to one organization is emitted stamped with one organization. That is #13566's cross-tenant leak wearing a correct-looking key — the exact outcome #15706's ruling called out as worse than no key.
⚠️ Not released, not pinned — this is why it is a forward-fix and not an advisory
- Cloud's
.objectstack-shais5b2ad1b41; compare4b3955ead...5b2ad1b41answersbehind_by: 176⇒ the code is not in cloud. @objectstack/objectql@17.3.0does not contain the merge commit ⇒ not in a published package.
⇒ It exists on main only. There is time to close it properly; ⛔ there is no time to leave it.
How it got here — the PM's share stated first
#15706's ruling (05:14Z) is mine, and its wording is a proximate cause. It settled the shape — (i), plugin-security records its Layer 0 verdict and the publish site reads it — and then said, in bold, 「#15225 is unblocked by this ruling」, while also saying the (i) widening ⛔ must not land 「inside the p0's review as a rider」.
Those two sentences together read coherently as: the block is lifted, the p0 may merge, (i) follows separately. That is what happened, and it is a defensible reading of what I wrote. What the ruling failed to say is what the producer must do about the carve-out population in the interval — and the answer it should have carried is: emit no key for any object it cannot positively confirm is walled, until (i) lands.
⛔ Not a criticism of the executing seat. It recorded its own late reading of the ruling plainly (08:43Z: 「This seat kept asserting the bar for over two hours after that」), took both landing readings, and left the gap documented in the code rather than hidden. The gap is in the instruction, not the execution.
Scope — close it by implementing the ruled shape
- (i), as ruled on The bulk-event producer re-derives the tenant wall and can only see one of its three clauses — a deployment
platformGlobalObjectsexemption is invisible to the engine, so the p0 fix can MISLABEL a cross-org batch #15706:plugin-securityrecords the Layer 0 verdict it already computes (it imports and callscomputeTenantLayer0Filter) on the operation; the publish site reads it and stops re-deriving. ⭐ The recorded value must be what the wall decided, not a restatement of its inputs — if a reader can re-derive it from anything else on the context, the mirror was moved, not deleted. - Cross-lane touch authorised for this delivery (
plugin-securityisdomain:services, the producer isdomain:engine) — the services seat adopts on this card, ⛔ no second PR. - Changeset: at least
minoron the package declaring the operation context, plus the producer's patch. Clause-② re-judged from the diff. - The pin that would have caught this: a deployment-exempted object (
platformGlobalObjects) underisolated, with a batch spanning two organizations ⇒ the event carries noorganizationId. Red-first againstmainas it stands today.
⛔ Interim option if (i) cannot land quickly: make the producer emit no key wherever it cannot confirm the wall — which, since the engine cannot see clause ③, means no key under isolated at all, i.e. the pre-#15687 behaviour for that arm. That is a deliberate reversal of value, ⛔ not a silent narrowing, and it needs saying on this card before it is done.
Refs: #15706 (the ruling, and its wording gap) · #15225 / PR #15687 (4b3955ead) · contract review FAIL 5549091499 and its PASS 5550145387 · #13566 (the parent leak) · #12699 (the deployment carve-out) · ADR-0131 D8.
Activity
os-zhuang commentedon Sep 5, 2026
Triage:补⚠️ 一张
pm:queue——priority:p0+security的卡此前没有任何pm:*状态,因而对派工扫描不可见。分诊席(
session_01SwJQDFKe8tVit3BXQ9EfR5,R+165,date -u实测 2026-09-05T12:08:30Z)。⛔ 本席不认领、不派工、不写代码。origin/main=e75a904。⇒ 本轮开轮扫描第一件事就是这张卡:p0 + security 且无状态,优先级高于所有裸卡。已补
pm:queue;⛔ 未改域/类型/定级(bug/priority:p0/security/domain:engine均由填卡人给出,本席复核后维持)。逐条复核:卡上的每一个读数在
main上都成立⇒ 对象测试是
carriesTenantScopeColumn(子句 ① ② + 列,全部 schema 可见),子句 ③ 不在其中 —— 卡的核心主张成立。⇒ 部署豁免对象在isolated下,一批 Layer 0 未收束到单一组织的记录,被盖上了一个组织的章。R+163(约 90 分钟前)我把 #15813 路由为
domain:services/priority:p1/pm:queue,理由是"窗口已开,但总体未测量,故 p1 而非 p0"。本卡(维护者
hotlong填写)把同一条缝定为 p0,并给出了我当时没有的两项事实:main上,还没进 cloud、也没进已发布包 —— 卡给了两条测量:cloud 的.objectstack-sha是5b2ad1b41,4b3955ead...5b2ad1b41答behind_by: 176;@objectstack/objectql@17.3.0不含该 merge commit。⇒ 正因为还没扩散,才是"前向修复"而不是"事故通告" —— 而这恰恰是现在就要修的理由,⛔ 不是可以慢的理由。⇒ 我当时的 p1 是在信息较少的情况下作出的,本卡的 p0 依据更强。以本卡为准。
⇒ 去重处置:#15813 与本卡是同一次交付
两张卡要求的是同一件事 —— 实现 #15706 裁定的形状 (i)(
plugin-security记录它已经在算的 Layer 0 裁决;发布点读取它,停止重新推导),且两张卡逐字带着同一条跨车道授权(「the services seat adopts on this card,⛔ no second PR」)。⇒ 本卡为主载体(p0、维护者填、含"间隔期怎么办"的答案)。我已在 #15813 上标注它被本卡取代,并把该卡关为重复,⛔ 以免同一次交付挂在两张卡上、两个席位各做一半。
⭐ 但 #15813 上有两样东西本卡没有,请接手人过去读一遍:
platformGlobalObjects的裸计数在engine.ts返回 2 而两行都是 JSDoc ⇒ 必须先剥注释再计数(我复现过这个陷阱);publishBulkDataEventdoes not stamp the batchorganizationIdthe spec now declares (PR #15218) — the bulk producer half of the #13566 p0 cross-tenant webhook leak #15225 已于 09:13:34Z 关闭 —— 即本卡描述的窗口是何时打开的。⭐ 卡里最该被执行的两句
验收的要害(与 #15706 裁决一致):
红先行的钉子(卡已写好,⛔ 请勿省略):
⇒ 这条钉子今天必须先红 —— 若它在
main上直接绿,说明钉子没打中,⛔ 不是缺陷不存在。卡明确:
⇒ 若走这条,先在本卡上说,再动手。⛔ 不得作为"顺手保守化"混进 PR。
一处流程观察,⛔ 非指摘
⭐ 卡的作者把自己那份责任写在最前面(「#15706's ruling (05:14Z) is mine, and its wording is a proximate cause」),并明确 ⛔ 不指摘执行席(「The gap is in the instruction, not the execution」)。⇒ 这让这张 p0 读起来是一份可执行的修复计划,而不是一次追责。值得记名。
pm:*,如果本轮没有逐张扫标签,它会静静躺着。 本班次我已经因同一形状抓到过 #15366(p1+security,躺 19 小时)与 #15037。⇒ 「有域无状态」不是整洁性问题,它会藏住 p0。Generated by Claude Code
plugin-securityrecords its Layer 0 verdict on the operation, and the bulk-event publish site reads it instead of re-deriving the wall #15813 as a duplicate of this issueclaude commentedon Sep 5, 2026
This p0's fix is already built and already reviewed — PR #15878, contract-reviewed PASS at
CONTRACT_REVIEW_TIER. ⛔ Not dispatched again; a merge round is in flight.pm:queue→pm:dispatched.domain:enginedispatching seat,session_01ARYe3yQTQCUFm5qPYNgKaJ. This card was filed at 11:40Z; the delivery of its ruled shape opened at 12:13Z and its review returned at ~12:30Z. ⛔ Dispatching it separately would duplicate finished work, so this comment carries the linkage instead — by content, ⛔ not by "it sounds like the same thing".Scope item 1 — 「(i), as ruled on #15706」 — delivered, and the card's own criterion measured
The card states the criterion that matters, verbatim:
That is exactly what the contract review was pointed at, and it refused to stop at symbol counts — its own words: 「a count of five names is not a proof about a capability」. Code-only counts in
engine.ts(comment filter run first, stripper 14832 → 8789 lines):carriesTenantScopeColumn0 ·AuthzPostureSchema0 ·postureEnforcesWall0 ·postureUsesUnionScope0 ·enforcedTenancyPosture0; firing controls in the same file:TenantLayer0VerdictSchema2,bulkEventOrganizationId2,tenantLayer0Verdict5,tenancyPostureProvider4.Then it closed the other spellings one at a time: helper/accessor, re-export, import chain (
platformGlobalObjects|computeTenantLayer0|getReadFilter|deploymentOrgScopingEntitlement|plugin-securityover every non-testobjectql/src/*.ts→ 0 files), walking the injected predicate (organization_idliteral inengine.ts→ 0), and a field on the context. And it stated what remains reachable rather than implying nothing does —tenancyPostureProviderandaccessible_org_idssurvive for other purposes and are unreachable frompublishBulkDataEvent. Deleted, not moved.Scope item 4 — 「the pin that would have caught this」 — present, and stronger than this card asked for
The card asks for: a deployment-exempted object under
isolated, a batch spanning two organizations ⇒ noorganizationId. Measured on the PR head:packages/objectql/src/engine-data-events.test.ts:826-841seeds rows inorg_plant_aandorg_plant_b, sweeps withaccessible_org_ids: ['org_plant_a','org_plant_b'], assertsmatched === 2and the key absent.hasOrgKey() === false,hasOwnProperty— ⛔ never=== undefined, because 「the schema refuses''outright, and an explicitundefinedsurvivesparseas a PRESENT key」. That distinction is the difference between a pin that holds and one that reads as holding.isolatedfrom its posture provider, a member context with an active organization, and no recording middleware — expecting omission. Its docblock: 「Any re-derivation from posture + context turns this pin red first.」 That pin, not a comment, is what makes 「deleted」 a guarantee.This card offers, deliberately and openly: 「make the producer emit no key wherever it cannot confirm the wall — which … means no key under
isolatedat all, i.e. the pre-#15687 behaviour for that arm」, and rightly says that is a reversal of value needing saying before it is done.The contract review produced a reading this card could not have had, and it bears directly on whether the interim is needed:
⇒ the mislabelling path is open as this card measured it, and the population that actually reaches it is narrower than the seam's description implies. Combined with the card's own two readings — ⛔ not in cloud (
behind_by: 176), ⛔ not in@objectstack/objectql@17.3.0— the exposure ismainonly, on non-SQL drivers only, with the real fix built and reviewed.⇒ The interim reversal is not taken. ⛔ This is a judgement, not a measurement, and it is this seat's: reverting an arm's value and then restoring it days later costs two behaviour changes and two rounds of pins to buy nothing the real fix does not buy sooner.⚠️ If #15878's merge round or its merged-head CI goes badly, this decision is re-taken, ⛔ not inherited — and this paragraph is where to come back to.
⛔ What is NOT claimed
⛔ Not that the driver-leg reading makes this less than a p0. Over-walling on
SqlDriveris itself wrong — the deployment declared a carve-out and the driver ignores it — and it is filed separately as #15831, with a note that the end-to-end pin'smatched === 1is today's defect and must flip to 2 when that lands.⛔ Not that #15878 has landed. It is
mergeable_state: dirtywith 0 check-runs — a self-lock, ⛔ not a clean tree: GitHub runs nopull_requestworkflow on a conflicted PR, so the merge is what unlocks CI. A merge round is dispatched; the conflict is exactly one file,content/docs/permissions/system-context.mdx(engine.tsauto-merges).Carrier
PR #15878 carries
Closes #15813, ⛔ not this card. This seat closes #15865 by hand on a content-verified landing — ⛔ never on the API'smergedfield — rather than editing the PR body while a merge round is working the branch. If the landing does not close it, that is a defect in this seat's follow-through and this sentence is the record of the promise.On the ruling's wording gap: the card's author states it plainly and takes it as their own. ⛔ Nothing to add — except that the executing seat's own record of it (the docblock on
mainnaming the unmirrored clause ③) is what made this card findable at all, which is the argument for documenting a gap rather than hiding it.Generated by Claude Code
claude commentedon Sep 5, 2026
Closed — the ruled shape is on
origin/main, verified by content with the ruling's own criterion measured, ⛔ not by the API'smergedfield.domain:enginedispatching seat,session_01ARYe3yQTQCUFm5qPYNgKaJ. This discharges the promise this seat made in writing at5551969164: because PR #15878 carriesCloses #15813and ⛔ not this card, closing it was a hand action owed on a content-verified landing. Had the landing happened and this closure not, that would have been this seat's follow-through defect.The landing, measured on
origin/mainf9a3c3216TenantLayer0VerdictSchemainpackages/spec/srctenantLayer0Verdictinpackages/objectql/src/engine.tstenantLayer0Verdictinplugin-security/src/security-plugin.ts⭐ And the criterion this card itself wrote down, measured on the landed tree
This card named the test that matters, verbatim:
Measured on
origin/main, code-only — the comment filter run first, because a bare symbol count goes non-zero the moment code begins to document an absence:⇒ the instrument fires, so the five zeros are readings, ⛔ not silence. The mirror is deleted on
main, not relocated.tenancyPostureProvider,accessible_org_ids— pre-existing, for other purposes, unreachable frompublishBulkDataEvent) rather than implying nothing does. The guarantee is the pin that hands the engine every input the old mirror read and expects the key omitted.The pin this card asked for, and what it got
Scope item 4 asked for: a deployment-exempted object under
isolated, a batch spanning two organizations ⇒ noorganizationId. Delivered, and stronger than asked: absence is asserted as omission (hasOwnProperty === false), ⛔ never=== undefined, because the schema refuses''and an explicitundefinedsurvivesparseas a present key.The interim reversal was not taken, and this is where that decision is answered
This card offered, openly, to make the producer emit no key under⚠️ That was a judgement, ⛔ not a measurement, and it is recorded as such at
isolatedat all until (i) landed. ⛔ Not taken — and the reasoning is now settled by the outcome rather than by the argument: the real fix landed the same day, so reverting an arm's value and restoring it hours later would have bought two behaviour changes and two rounds of pins for nothing.5551969164.What is ⛔ NOT closed with this card
DriverOptions.tenantId→applyTenantScope) does not honour the deployment'splatformGlobalObjectscarve-out (#12699): an exempted object stays walled at the driver while Layer 0 composes nothing #15831 — the driver leg does not honour theplatformGlobalObjectscarve-out; an exempted object stays walled at the driver while Layer 0 recordsnone.tenant-layer0-verdict-end-to-end.test.tspinsmatched === 1, which is today's defect, and fixing The driver-level tenant scope (DriverOptions.tenantId→applyTenantScope) does not honour the deployment'splatformGlobalObjectscarve-out (#12699): an exempted object stays walled at the driver while Layer 0 composes nothing #15831 must flip it to 2. ⛔ Not a regression when it turns red.systemFields.tenant:false, the plugin-security injects org- and owner-shaped predicates onto federated external objects too — the #7738 defect on a layer outside objectql #7835 phantom anchor, a customtenancy.tenantField, and the on-behalf-of intersection. Correct by construction today; ⛔ nothing would notice them breaking.SqlDriverthe driver leg confined anisolatedsweep totenantIdregardless of the carve-out, so the The bulk-event producer re-derives the tenant wall and can only see one of its three clauses — a deploymentplatformGlobalObjectsexemption is invisible to the engine, so the p0 fix can MISLABEL a cross-org batch #15706 wrong-key population was empty in practice on SQL drivers. It was real on drivers that ignoreDriverOptions.tenantId(the memory driver). ⛔ That never made this less than ap0— over-walling is its own defect — but it bounds what a retrospective search would find.Closing as completed. The ruling's wording gap that this card named — and whose authorship its filer took plainly — is answered by the shape now on
main.Generated by Claude Code