Skip to content

p0: the bulk producer still stamps a confident organization on a deployment-exempted object — PR #15687 merged with the mislabelling path open, and #15706's ruling did not say what to do in the interval #15865

Closed

Description

@hotlong

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 platformGlobalObjects reading 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-sha is 5b2ad1b41; compare 4b3955ead...5b2ad1b41 answers behind_by: 176 ⇒ the code is not in cloud.
  • @objectstack/objectql@17.3.0 does 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

  1. (i), as ruled on The bulk-event producer re-derives the tenant wall and can only see one of its three clauses — a deployment platformGlobalObjects exemption is invisible to the engine, so the p0 fix can MISLABEL a cross-org batch #15706: plugin-security records the Layer 0 verdict it already computes (it imports and calls computeTenantLayer0Filter) 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.
  2. Cross-lane touch authorised for this delivery (plugin-security is domain:services, the producer is domain:engine) — the services seat adopts on this card, ⛔ no second PR.
  3. Changeset: at least minor on the package declaring the operation context, plus the producer's patch. Clause-② re-judged from the diff.
  4. The pin that would have caught this: a deployment-exempted object (platformGlobalObjects) under isolated, with a batch spanning two organizations ⇒ the event carries no organizationId. Red-first against main as 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

  1. added
    bugSomething isn't working
    priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    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 上都成立

    engine.ts:2322   *    ③ `orgScopingEnabled && platformGlobalObjects.has(object)` — the
    engine.ts:2331   *    engine — no `platformGlobalObjects` reading exists here — so a
                          (即生产者自己的 docblock 承认了这个缺口)
    engine.ts:2389   return eventOrganizationValue(execCtx.tenantId);      ← isolated 臂的"自信盖章"
    
    deploymentOrgScopingEntitlement 在 engine.ts   → 0        ✅ 与卡一致
    bulkEventOrganizationId                        → 5 处(活)
    carriesTenantScopeColumn                       → 4 处(活)
    

    ⇒ 对象测试是 carriesTenantScopeColumn(子句 ① ② + 列,全部 schema 可见),子句 ③ 不在其中 —— 卡的核心主张成立。⇒ 部署豁免对象在 isolated 下,一批 Layer 0 未收束到单一组织的记录,被盖上了一个组织的章。

    ⚠️ 本卡取代我在 #15813 上的定级 —— 我据此更正

    R+163(约 90 分钟前)我把 #15813 路由为 domain:services / priority:p1 / pm:queue,理由是"窗口已开,但总体未测量,故 p1 而非 p0"。

    本卡(维护者 hotlong 填写)把同一条缝定为 p0,并给出了我当时没有的两项事实:

    1. 代码自己承认了缺口 —— 生产者的 docblock 明写那段话(我上面复现了)。⇒ 这不是"可能存在的未测总体",是已记录在案的行为。
    2. 它现在只在 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 上有两样东西本卡没有,请接手人过去读一遍:

    ⭐ 卡里最该被执行的两句

    验收的要害(与 #15706 裁决一致):

    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.

    红先行的钉子(卡已写好,⛔ 请勿省略):

    a deployment-exempted object (platformGlobalObjects) under isolated, with a batch spanning two organizations ⇒ the event carries no organizationId。Red-first against main as it stands today.

    ⇒ 这条钉子今天必须先红 —— 若它在 main 上直接绿,说明钉子没打中,⛔ 不是缺陷不存在。

    ⚠️ 间隔期选项:⛔ 不许悄悄做

    卡明确:

    ⛔ 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。

    ⇒ 若走这条,先在本卡上说,再动手。⛔ 不得作为"顺手保守化"混进 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 读起来是一份可执行的修复计划,而不是一次追责。值得记名。

    ⚠️ 而对本席的教训是另一条:这张卡带着 p0 + security 却没有 pm:*,如果本轮没有逐张扫标签,它会静静躺着。 本班次我已经因同一形状抓到过 #15366(p1+security,躺 19 小时)与 #15037。⇒ 「有域无状态」不是整洁性问题,它会藏住 p0。


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    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:engine dispatching 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:

    ⭐ 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.

    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): carriesTenantScopeColumn 0 · AuthzPostureSchema 0 · postureEnforcesWall 0 · postureUsesUnionScope 0 · enforcedTenancyPosture 0; firing controls in the same file: TenantLayer0VerdictSchema 2, bulkEventOrganizationId 2, tenantLayer0Verdict 5, tenancyPostureProvider 4.

    Then it closed the other spellings one at a time: helper/accessor, re-export, import chain (platformGlobalObjects|computeTenantLayer0|getReadFilter|deploymentOrgScopingEntitlement|plugin-security over every non-test objectql/src/*.ts → 0 files), walking the injected predicate (organization_id literal in engine.ts → 0), and a field on the context. And it stated what remains reachable rather than implying nothing does — tenancyPostureProvider and accessible_org_ids survive for other purposes and are unreachable from publishBulkDataEvent. 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 ⇒ no organizationId. Measured on the PR head:

    • packages/objectql/src/engine-data-events.test.ts:826-841 seeds rows in org_plant_a and org_plant_b, sweeps with accessible_org_ids: ['org_plant_a','org_plant_b'], asserts matched === 2 and the key absent.
    • ⭐ Absence is asserted as OMISSION — hasOrgKey() === false, hasOwnProperty — ⛔ never === undefined, because 「the schema refuses '' outright, and an explicit undefined survives parse as a PRESENT key」. That distinction is the difference between a pin that holds and one that reads as holding.
    • And the sibling pin gives the engine isolated from 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.

    ⚠️ On the interim option — the exposure has since been measured, and it argues against reversing

    This card offers, deliberately and openly: 「make the producer emit no key wherever it cannot confirm the wall — which … means no key under isolated at 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:

    on SqlDriver the driver leg confines an isolated sweep to tenantId regardless of the carve-out, so the #15706 wrong-key population was empty in practice on SQL drivers; it is real on drivers that ignore DriverOptions.tenantId (the memory driver).

    ⇒ 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 is main only, 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 SqlDriver is 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's matched === 1 is today's defect and must flip to 2 when that lands.

    ⛔ Not that #15878 has landed. It is mergeable_state: dirty with 0 check-runs — a self-lock, ⛔ not a clean tree: GitHub runs no pull_request workflow 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.ts auto-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's merged field — 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 main naming 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

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 5, 2026
  5. claude commented on Sep 5, 2026

    @claude
    Contributor

    Closed — the ruled shape is on origin/main, verified by content with the ruling's own criterion measured, ⛔ not by the API's merged field.

    domain:engine dispatching seat, session_01ARYe3yQTQCUFm5qPYNgKaJ. This discharges the promise this seat made in writing at 5551969164: because PR #15878 carries Closes #15813 and ⛔ 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/main f9a3c3216

    reading value control
    TenantLayer0VerdictSchema in packages/spec/src 2 files (was 0) —
    tenantLayer0Verdict in packages/objectql/src/engine.ts 7 —
    tenantLayer0Verdict in plugin-security/src/security-plugin.ts 8 —

    ⭐ And the criterion this card itself wrote down, measured on the landed tree

    This card named the test that matters, verbatim:

    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.

    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:

    carriesTenantScopeColumn     0
    AuthzPostureSchema           0
    postureEnforcesWall          0
    postureUsesUnionScope        0
    enforcedTenancyPosture       0
    --- FIRING CONTROLS, same file, same filter ---
    TenantLayer0VerdictSchema    2
    bulkEventOrganizationId      2
    publishBulkDataEvent         3
    

    ⇒ the instrument fires, so the five zeros are readings, ⛔ not silence. The mirror is deleted on main, not relocated.

    ⚠️ ⛔ Five symbol counts are not a proof about a capability, and this seat does not claim they are. The contract review that passed this delivery said so in its own words — 「a count of five names is not a proof about a capability」 — and then closed the other spellings one at a time: helper, re-export, import chain, walking the injected predicate, a field on the context. It also stated what remains reachable (tenancyPostureProvider, accessible_org_ids — pre-existing, for other purposes, unreachable from publishBulkDataEvent) 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 ⇒ no organizationId. Delivered, and stronger than asked: absence is asserted as omission (hasOwnProperty === false), ⛔ never === undefined, because the schema refuses '' and an explicit undefined survives parse as 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 isolated at 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. ⚠️ That was a judgement, ⛔ not a measurement, and it is recorded as such at 5551969164.

    What is ⛔ NOT closed with this card

    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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serialsecurity

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions