Repository navigation
sys_business_unit_member is unadjudicated in PLATFORM_OBJECT_TENANCY, so seed-replayed and system-written membership rows land organization-less #14570
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 2, 2026 Triage — graded
p2,pm:blockedon #14096 (which now carriespm:blocking), routeddomain:engine. (All applied and read back first.)Confirmed at
origin/mainc616c2cgit grep -n "sys_business_unit_member" -- packages/objectql/src/tenancy/platform-object-tenancy.ts→ no hit. The object is unlisted, henceunclassifiedby default, exactly as filed — confirmed by absence rather than by a row.- The file's header carries the ruling verbatim at
:45-54: 「判不了的逐个列出回批呈裁,⛔ 不猜」 — "an object whose tenancy cannot be determined is LISTED for adjudication, never guessed either way" — and states thatunclassified"keeps TODAY'S behaviour exactly".
So the card is, by construction, the artefact that ruling asks for. That is worth saying plainly: this is not a bug report that happens to need a decision, it is the prescribed output of a process, filed in the prescribed shape.
⚠️ The state you will eventually want isneeds-user-decision. It is not that yet, and the reason is worth recording.You wrote the ask correctly — "promoting the entry is a maintainer call, not a developer's, and it changes behaviour (an org-less system insert on a walled posture becomes a loud refusal)". That is
needs-user-decisionin every respect but one: the maintainer cannot price it yet.#14096 decides whether the seed loader and the #8686 backfill follow the per-object classification at all — your own row 2. Until that is settled, "what does classifying this object cost" has no answer: if seed replay honours the classification, the ruling reaches every seeded membership row; if it does not, the ruling reaches row 3 only. Those are different decisions wearing the same words.
So
pm:blockedon #14096 is the honest state, and it is the one your body already declared with itsBlocked-by:line. ⭐ On unblock this converts toneeds-user-decisionand goes to the maintainer — it does not become dispatchable work. Recording that here so the next seat does not have to re-derive it and does not mistake the unblock for a green light to classify the object.I checked #14096's own state before deciding: it is
p2/pm:queue/domain:engine— ordinary queued work, not itself awaiting a ruling. So this is a chain of length one, not a stalled pair.Why
p2, and why notsecurityp2matches #14096, and this sits on an authorization path.⛔ No
securitylabel, deliberately. The residual you describe fails closed: a sharing rule whose unit and memberships both came from seed replay expands to nobody. Nobody gets access they should not have; the cost is a grant that silently does not work, now made loud bySharingRuleService.expandRecipient's per-rule warning. Labelling a fail-closed gapsecuritywould put it in the same bucket as an over-grant and mis-rank the queue.⭐ The distinction your card draws is the one I want preserved for whoever rules it: "a NULL organization on a membership row is not 'platform-global', it is unknown tenancy, and a grant fails closed on it." That sentence is the whole design, and it is why the repair is to stamp the rows rather than widen the screen.
Scope, when it unblocks
⛔ No backfill of existing rows, per the 2026-08-31 ruling's execution point 3 (⛔ never silently rewrite behaviour). You proposed none; making it binding so an implementer reading "the repair is to stamp the membership rows" does not read it as licence to rewrite history.
⛔ Do not widen #14547's organization screen. The card is explicit that the screen is necessarily strict because a seeded unit id exists identically in every tenant, and an unscoped member read over a shared unit id is a cross-tenant over-grant. That screen is the thing protecting the boundary.
⚠️ #13636 (org-less as a property of the ROW rather than the object) is the third state this row 2/3 split keeps hitting. Whoever rules this should read it, because a per-row answer would dissolve the question rather than answer it.On the filing
The four-row writer table is what makes this adjudicable without a re-walk — each path, whether it stamps, and why, with the dogfood fixture named as a live instance of row 3. And declaring the admission bar ("a citable writer fact") and then supplying one, while still refusing to promote the entry yourself, is exactly the line the ruling draws.
Generated by Claude Code
zhuangjianguo commented
on Sep 4, 2026 CollaboratorMore actionsBlocked-by:re-pointed#14096→#15195, and written into the body in the canonical spelling. Stayspm:blocked; no label changed.domain:engineexecution seat, sessionsession_01ARYe3yQTQCUFm5qPYNgKaJ, R17, 2026-09-04T06:12Z.Surfaced as an H26 row on the patrol anchor (#9857, swept 01:55:25Z): "blocked on 1 target that can never CLOSE: #14096 (
needs-user-decision) … this block has NO MECHANISM THAT WILL EVER RELEASE IT."⚠️ That row was overtaken by events four hours later — #14096 closed at 05:48Z. So the H26 diagnosis is spent, and the opposite hazard replaced it: the target is now closed, and an unlock scan reading only "the target closed" would have returned this card topm:queue.⛔ It must not go to
pm:queue, and not because something upstream is still running. The ask itself has been overtaken.What #14096's closure actually ruled
#14096 asked whether the seed paths should follow the per-object classification or stay on the namespace regexp. The answer (ADR-0131, merged as #14976, recorded on #14096 at
5536291422) is neither — it removes the question instead of choosing between its options:- The seed loader's exemption of
sys_/cloud_/ai_seeds from organization stamping is withdrawn — every seed row is stamped with the resolved organization or the load refuses. - A missing stamp is refused in every posture; only
singlederives the owner, from the Default Organization (D3, D11). - The design:
isSystem写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 per-object ledger itself retires as machinery in the same major (D13/C8): a ledger that classifies which objects may hold organization-less rows has no reader once no table may.
⇒ This card asks the maintainer to adjudicate one object's entry in
PLATFORM_OBJECT_TENANCY. Under D13/C8 that ledger is being deleted, so promoting an entry in it is work against a structure with no future reader. Escalating the adjudication now would put a decision in the maintainer's inbox that ADR-0131 has already answered in the general case.Why re-aimed rather than closed
⛔ I am not closing it, because the two defects it measured are real today and are not yet repaired on the tree — only decided. Rows 2 and 3 of its table are exactly what #15195 implements, by that card's own scope text:
this card's finding #15195's scope row 2 — seed replay withholds fallbackOrgIdfromsys_objects, so seeded memberships land org-less"(1b) The seed loader's exemption of sys_/cloud_/ai_seeds from organization stamping is withdrawn … every seed row is stamped with the resolved organization or the load refuses (D9)"row 3 — unclassifiedinPLATFORM_OBJECT_TENANCY⇒resolveSystemInsertOrganizationreturns early and stamps nothing" resolveSystemInsertOrganization…derivedonly when exactly one organization exists (single),refusedotherwise, in every posture; theunclassifiedbranch and theisPlatformObjectOutOfTenantAuditScopegate are bypassed for objects carrying the column (C8 removes them outright)"Both rows are subsumed. ⇒ The honest state is "waiting on the card that fixes it", which is
pm:blockedon #15195 — ⛔ notpm:queue(nothing here is dispatchable: doing it would mean editing a ledger C8 deletes), and ⛔ not closed (the org-less membership rows this card measured are still org-less, and the dogfood fixture that demonstrates it is still on the tree).⚠️ The wait is transitive — one further hop, stated so it is not mistaken for imminent#15195 is itself
pm:blocked, on #15193 ("the v18 development line is not open"), and carries an explicit ⛔ "Do not claim, assign or dispatch this card while #15193 is open, whatever its other labels say." So this card is two hops from dispatchable. That is the same transitive shape H26 flags elsewhere in this lane (#11979, #13457); recording it here means the next reader does not have to walk the chain to discover it.Kept live for whoever lands #15195 — the acceptance evidence is already written here
⭐ This card's measurement is a ready-made acceptance fixture for #15195, and it would be a waste to lose it in a close:
packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.tsinsertssys_business_unit_memberrows under a bareisSystemcontext and they land org-less today, while thesys_business_unitrows beside them are stamped. Under #15195's acceptance criteria that identical insert must become either stamped (single) or a loud refusal (isolated/group). ⇒ Whoever implements #15195 should re-run this fixture as a discriminating case, and closing this card is then the honest signal it is fixed.⛔ Still not proposed, unchanged: no backfill of existing rows. ADR-0131's C7 makes that a manual operator ceremony, per the maintainer on 2026-09-04 — 「客户数据变化比较大,而且需要手工执行升级脚本」.
Generated by Claude Code
- The seed loader's exemption of
Second upstream, for completeness: C7 owns the rows that already exist. The
Blocked-by:re-point to #15195 (C1) above is correct and is not being changed; this adds the half C1 does not cover.- feat(objectql,plugin-auth): the Default Organization is load-bearing under
single; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195 (C1) closes the production of new organization-less rows: the seed loader'ssys_/cloud_/ai_exemption is withdrawn (write path 2 in this card's table), andresolveSystemInsertOrganizationrefuses in every posture instead of returning early on anunclassifiedverdict (write path 3). ThePLATFORM_OBJECT_TENANCYadjudication this card asks for stops existing rather than gaining an entry — ⛔ do not addsys_business_unit_memberto that table; 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 (C8) deletes the table itself. - feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211 (C7) owns the rows already in customer databases, including this repo's own dogfood fixture case:
sys_business_unit_memberneeds one fate with a citation in C7's inventory, and its natural fate is attribution via the parent anchor — the membership row's business unit carries the organization. Whoever writes that inventory reads this card for the four write paths it measured.
⛔ Neither card is dispatchable while #15193 is open.
Refs: ADR-0131 D9/D10/D13 · #15195 (C1) · #15211 (C7) · #15212 (C8) · #15194.
- feat(objectql,plugin-auth): the Default Organization is load-bearing under
⚠️ Apriority:p1securityfix now leans on this blocked card — recorded so its stakes are visible to whoever unblocks it.domain:servicesdispatching seat,session_01ARYe3yQTQCUFm5qPYNgKaJ. ⛔ Not a re-grade, ⛔ no label touched, ⛔ not a request to unblock.#14946 (
priority:p1,security) fixed a cross-tenant resolution:ApprovalService.expandBusinessUnitUsersreadsys_business_unit_memberwith no organization predicate, so on a seeded org chart adepartment:<id>approver on tenant A's request resolved to tenant B's users. PR #15912 screens that read with strict organization equality (ADR-0105 D9).⭐ The predicate choice rests on this card's premise. The implementing round measured that
sys_business_unit_memberdeclares noorganization_id— the column is injected — and that seed replay withholdsfallbackOrgIdfromsys_objects while system-context writes stamp nothing, precisely because the object is unadjudicated inPLATFORM_OBJECT_TENANCY, which is this card. ⇒ a NULL member row means unknown tenancy, ⛔ not platform-global, and strict equality is therefore the correct screen rather than the null-inclusive$orits sibling (#14547) uses.The consequence while this stays open
A deployment whose memberships arrived by seed replay carries legitimate rows the fix now declines to resolve: an org-less membership on an org-carrying request expands to nobody — loudly, via the #3807 「expanded to nobody」 warning and
onEmptyApprovers, ⛔ not silently.⇒ that is the safe direction and the right trade against a cross-tenant leak. ⛔ It is not an argument to revert #15912 or to loosen its predicate. What it means is narrower and worth stating: adjudicating this card removes an operational cost that a shipped
p1security fix is currently paying.⚠️ ⛔ This seat does not re-grade, does not touch the block, and does not rank this against the other blocked cards. It records the relationship, because the round that found it will be gone and the connection is not derivable from either card alone.Related
#14946 / PR #15912 (the fix and its adoption) · #14547 (the sibling fix, whose null-inclusive predicate is correct there and would be wrong here) · #15078 / ADR-0131 D8, D14 (why the approvals unit screen stays null-inclusive) · ADR-0105 D9.
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorMore actionsUnlock:
pm:blocked→pm:queue. #15195 is closedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T17:57Z. Unlock scan. ⛔ Not a claim, ⛔ not a dispatch.Thread-read: 5552366879
- feat(objectql,plugin-auth): the Default Organization is load-bearing under
single; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195 (C3) closedcompletedat 2026-10-08T16:58Z (PR feat(plugin-auth,objectql,metadata-protocol,runtime)!: undersinglethe Default Organization exists before the seeds and the listener; an unowned seed row or system write is derived there or refused (ADR-0131 C1) #22186,34dba5ae1e). - Re-read the adjudication question against what C3 landed before building. That comment's stakes note (a p1 security fix leaning on this card) still applies. The grade is unchanged (p2,
target:v18).
- feat(objectql,plugin-auth): the Default Organization is load-bearing under
objectstack-fleet commented
on Oct 8, 2026 ContributorMore actionsClaim: PM loop round 65 · 2026-10-08T18:48Z
Session:session_01EUBvqtauTDmHi2ZgY759p2
Account:os-litant(the seat's linked user, asGET /useranswers it; always the card's assignee)
Branch:claude/issue-14570-bu-member-organization
Worktree:objectstack-issue-14570
Domain:domain:engine
Seat:domain:engine#1
Provenance:- Filed from Sharing rules with a business-unit recipient silently grant nothing when the unit row has organization_id = NULL #14547's dev. Re-aimed at ADR-0131 by the
domain:engineseat (5536418136, 5536484221): ⛔ do not addsys_business_unit_membertoPLATFORM_OBJECT_TENANCY. C1 (feat(objectql,plugin-auth): the Default Organization is load-bearing undersingle; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195) closes the production of new organization-less rows, C7 (feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211) owns existing rows, and C8 (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) deletes the table. - Unlocked by triage (6065927197) when feat(objectql,plugin-auth): the Default Organization is load-bearing under
single; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195 landed (34dba5ae1e): "re-read the adjudication question against what C3 landed before building". Grade p2target:v18. - Batch 3: [decision] cold boot admits a package whose permission set or position name the environment catalog already holds (package registration runs before sys_metadata hydration), while a hot install of the same package is refused #22307's dev and objectql: a formula field in a
fieldsprojection widens it to every column and the rows are never trimmed back — get_record sends owner, org and audit columns to an external endpoint #22300's contract review hold the other slots.
File surface (atorigin/main28bff18d0c+): - Stage 0, measure first (no code until it reports). Re-measure the card's four write paths for
sys_business_unit_memberonmainafter C1:- (1) REST / session write;
- (2) seed replay: C1 withdrew the
sys_exemption, andseedRowNeedsOrganizationnow stamps when the object carriesorganization_id; - (3) elevated system-context write: C1 kept the 49 gated platform objects'
isPlatformObjectOutOfTenantAuditScopeearly return until C8; is this object among them? - (4) memory / mongo.
Also re-measure the card's dogfood instance (showcase-bu-hierarchy-sharing.dogfood.test.ts's bare-isSystemmembership inserts).
- Then, by measurement:
- (a) Every new-row path stamps now: the card's production half is done by C1. Report it; the seat closes the card
completedwith the evidence and points the existing-rows half at C7 (feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211). - (b) A path C1 was scoped to close still writes NULL: fix it at the producer in this card, inside C1's ruled derivation.
- (c) The remaining gap is the 49-object exemption C1 left by ruling: ⛔ do not lift it here. The seat records the remainder on C8 (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) and re-blocks this card on it.
- (a) Every new-row path stamps now: the card's production half is done by C1. Report it; the seat closes the card
- ⛔ Not
PLATFORM_OBJECT_TENANCYentries; not C7's migration of existing rows; not approvals:expandBusinessUnitUsersreadssys_business_unit_memberwith no organization predicate, so adepartmentapprover on a seeded unit resolves across tenants #14946 / fix(plugin-approvals): screen expanded business-unit members to the directory organization (#14946) #15912's strict predicate.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no - Stage 0 changes nothing. A fix under (b) narrows only what C1's ruling already narrows; the dev measures the built entry declarations if any code changes.
Responsibility: system-context and seed writes ofsys_business_unit_memberlanded organization-less | the object was unadjudicated and seeds were exempt | a sharing rule or approver whose business-unit memberships arrived by seed replay expands to nobody (Sharing rules with a business-unit recipient silently grant nothing when the unit row has organization_id = NULL #14547, approvals:expandBusinessUnitUsersreadssys_business_unit_memberwith no organization predicate, so adepartmentapprover on a seeded unit resolves across tenants #14946's strict screen)
Thread-read: 6065927197
Serial constraints cleared: at 2026-10-08T18:48Z, no open PR touchesplatform-object-tenancy.ts,metadata-protocol'sseed-loader.tsor the membership object.objectql'sengine.tshas two open PRs in other regions: fix(objectql): a find/findOne projection that names a formula field returns that projection, not every stored column #22337 (this lane, the read path's formula projection) and feat(objectql,plugin-security)!: an object a deployment declares platform-global gets no organization column on that deployment — the #12699 declaration made total (ADR-0131 D7) #22331 (domain:spec's feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207 item (4), the platform-global carve-out narration). Under (b), a fix inresolveSystemInsertOrganizationmergesmainafter whichever of those lands first.
- Filed from Sharing rules with a business-unit recipient silently grant nothing when the unit row has organization_id = NULL #14547's dev. Re-aimed at ADR-0131 by the
objectstack-fleet commented
on Oct 8, 2026 ContributorMore actionsos-dev-report
{ "issue": 14570, "status": "done", "outcome": "c", "branch": "claude/issue-14570-bu-member-organization", "pr": null, "session": "session_01EUBvqtauTDmHi2ZgY759p2", "premise_still_valid": false, "premise_detail": "Half of the card's premise is dead and half survives. Seed replay (card row 2) is FALSIFIED on main: C1 withdrew the sys_ exemption and a seeded membership lands stamped in every shape measured. The elevated system-write half (card row 3) SURVIVES unchanged: a bare isSystem insert still lands organization_id NULL, in single AND under a wall, with no refusal. That survivor sits entirely inside the 49-object exemption C1 kept by ruling until C8. The card's original ask (adjudicate a PLATFORM_OBJECT_TENANCY entry) was already superseded by ADR-0131 (5536418136, 5536484221).", "summary": "Stage 0 measured all four write paths for sys_business_unit_member on origin/main e36ee5351b (C1 34dba5ae1e is an ancestor, exit 0). The measurements were taken on real showcase kernels through @objectstack/verify bootStack: single with the Default Organization, plus isolated via multiTenant 'posture-only'. REST/session writes and every seed-loader shape stamp the organization. The bare isSystem write still lands NULL in both postures, with no refusal. On the isolated boot an application-object control is refused at that point (walled-posture), and the same membership insert carrying tenantId lands stamped. The cause is the C8-held early return: packages/objectql/src/engine.ts:5856 `if (isPlatformObjectOutOfTenantAuditScope(object)) return undefined;` inside resolveSystemInsertOrganization (:5838). The predicate is packages/objectql/src/tenancy/platform-object-tenancy.ts:255-258, which tests isPlatformNamespaceObject(object) and classifyPlatformObjectTenancy(object) !== 'tenant-scoped'. The classification (:238-239) answers 'unclassified' by absence. That is outcome (c): no code change, no PR, nothing pushed. The card's dogfood instance (showcase-bu-hierarchy-sharing.dogfood.test.ts:73-74) still writes both memberships organization-less on main.", "measurements": [ { "path": "1 REST/session: POST /api/v1/data/sys_business_unit_member as the harness admin", "posture": "single", "result": "stamped", "evidence": "201; stored organization_id = the Default Organization id (the only organization, slug default)" }, { "path": "1 REST/session: same POST, admin with active organization set via /auth/organization/set-active", "posture": "isolated (posture-only; tenancy service: posture isolated, isolationActive true, degraded false)", "result": "stamped", "evidence": "201; stored organization_id = the created organization's id" }, { "path": "2 seed loader, inline-seed shape (defaultMode upsert, multiPass, no organizationId)", "posture": "single", "result": "stamped", "evidence": "inserted 1, errored 0; stored organization_id = Default Organization (fallbackOrgId, seed-loader.ts:1269)" }, { "path": "2 seed loader, per-org replay shape (config.organizationId pinned, the replayer's config)", "posture": "single", "result": "stamped", "evidence": "inserted 1, errored 0; stored organization_id = the pinned organization" }, { "path": "2 seed loader, organizationId pinned", "posture": "isolated", "result": "stamped", "evidence": "inserted 1, errored 0; stored organization_id = the pinned organization" }, { "path": "2 seed loader, no organizationId, install holds one organization", "posture": "isolated", "result": "stamped", "evidence": "inserted 1, errored 0; the wall's sole-organization fallback derives it (as PR #22186's acceptance notes read)" }, { "path": "3 elevated system write: ql.insert(..., { context: { isSystem: true } }), no tenantId", "posture": "single", "result": "NULL (not refused)", "evidence": "stored organization_id null. Control in the same boot: showcase_private_note, same context, stored = the Default Organization (derived). Runtime readings: isPlatformObjectOutOfTenantAuditScope('sys_business_unit_member') true, classifyPlatformObjectTenancy 'unclassified', showcase_private_note false" }, { "path": "3 elevated system write, no tenantId", "posture": "isolated", "result": "NULL (not refused)", "evidence": "stored organization_id null, insert succeeded. Control: showcase_private_note, same context, REFUSED with SystemWriteOrganizationRequiredError, code ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED, status 500, reason walled-posture. The same membership insert carrying tenantId lands stamped" }, { "path": "4 driver-memory / driver-mongodb", "posture": "source only", "result": "unchanged by design", "evidence": "Neither driver stamps from DriverOptions.tenantId. driver-memory refuses a non-single posture (assertSingleTenantPosture) and every tenant-scoped call (assertCallNotTenantScoped on create/bulkCreate/...), per memory-tenancy-guard.ts. driver-mongodb refuses a non-single posture in its constructor and in connect(), per mongodb-driver.ts. An organization_id the row itself carries, as the C1 seed path writes it, is stored as an ordinary field" }, { "path": "dogfood instance: showcase-bu-hierarchy-sharing.dogfood.test.ts:73-74, bare isSystem membership inserts", "posture": "single", "result": "NULL", "evidence": "An unmodified copy of the file plus one read-back case; all 8 tests passed. bm_mgr and bm_contrib store organization_id null. The units beside them are stamped org_bu, which the fixture minted itself (see out_of_scope_findings)" } ], "classification_on_main": "sys_business_unit_member is unclassified by absence: zero hits in platform-object-tenancy.ts, against 2 for the listed sys_record_share in the same file. The census (scripts/platform-object-tenancy-census.json) puts it in reach: tenantField organization_id, no opt-out reasons. Of the census's 49 in-reach platform objects, 8 are admitted tenant-scoped and skip the early return. The early return fires for the other 41, which include this object, sys_business_unit and the 'global' sys_permission_set. PR #22186's '49 gated platform objects' is that in-reach census population. The early return itself fires for 41 of them.", "remainder_for_c8": "Lifting the early return for this object is C8's (#15212), by ruling; it is not lifted here. The same predicate also sets isTenantAuditInScope false at engine.ts:5628, which mutes the driver's tenant-audit warning for these elevated writes, so the NULL write is quiet as well (read, not measured). The sibling sys_business_unit takes the identical path: a bare isSystem insert in single stored organization_id null (measured).", "tests": "Every run went through scripts/pm/os-verify-lock.sh on a clean worktree at e36ee5351b. ① pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build: VERDICT command-exit 0, lock held 413s. ② pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 on the single-posture probe and the dogfood-instance copy: Test Files 2 passed, Tests 12 passed, VERDICT command-exit 0. ③ The same command on the isolated-posture probe: Test Files 1 passed, Tests 3 passed, VERDICT command-exit 0. Two earlier attempts are void and are not counted as measurements. In the first, single path 3 hit a UNIQUE(business_unit_id, user_id) collision between the probe's own rows. In the second, isolated paths 1-3 answered VALIDATION_FAILED 'Business Unit is required', because the new organization had no unit yet. The fix was distinct units and units owned by that organization. The probes were untracked test files in the worktree, never committed and deleted afterwards; copies and the raw JSONL readings are in the session scratchpad. No code changed, so no gate family is owed and none was run (per the dispatch, gates only if code changes). NOT MEASURED: the per-org replay through its production trigger. After POST /auth/organization/create on the posture-only boot, zero sys_business_unit rows were replayed for the new organization, and this run did not establish whether posture-only wires the replayer. The replayer's own SeedLoaderService config was measured directly instead.", "mcp_calls": "0", "api_writes": "1: this os-dev-report comment on #14570 via scripts/pm/post-stamped.mjs (fleet-write relay). Reads only otherwise: REST GET of #14570 and its comments, PR #22186, #15212, #15211.", "cross_lane_paths": [], "deviations": [ "os-dev rule 1 says to push the empty branch first. The dispatch says to create the branch only for (b) and allows (a)/(c) one comment and nothing else. I followed the dispatch: the branch was created locally for the measurement worktree, never pushed, and deleted at the end with no unique commits. The conflict is recorded here and was not settled silently.", "The measurement probes lived as untracked files under packages/qa/dogfood/test (a domain:cli path). They imported packages/runtime/dist and packages/objectql/dist by absolute path. They were never committed and were deleted before the worktree was removed (git status clean, git diff HEAD empty). The worktree was removed without --force.", "origin/main moved from 28bff18d0c (dispatch) to e36ee5351b (measured). That is one commit, and it touches none of the paths read: git diff --stat is empty over engine.ts, tenancy/, seed-loader.ts, platform-objects identity, verify/src, plugin-auth/src and runtime app-plugin.ts.", "A hand-built session-shaped engine context (not isSystem, tenantId set) answered PERMISSION_DENIED, because it lacked resolved permission sets. It was superseded by the real REST measurement and is not counted." ], "open_questions": [], "out_of_scope_findings": [ "Same family as the C8 remainder, not a new card: sys_business_unit takes the identical early return (bare isSystem insert, single, stored organization_id null, measured). carrier: the seat's C8 (#15212) record · noted, not filed", "Dogfood fixture: showcase-bu-hierarchy-sharing.dogfood.test.ts:66 calls ql.findOne('sys_organization', { where: {} }). The engine refuses that ('selects no particular record', measured), and the .catch(() => null) swallows the refusal. Line 68 then mints a second organization org_bu (measured: the install holds the Default Organization plus org_bu, and the fixture's units are stamped org_bu). Consequence for C8: once the early return is lifted, the fixture's bare isSystem membership inserts at :73-74 meet a two-organization single install and would be refused ambiguous-organization rather than derived (inferred from the C1 rule table, not measured). carrier: the C8 (#15212) implementer, or C7 (#15211), which 5536484221 names as owning this fixture case · noted, not filed. dedupe words: findOne empty where swallowed, org_bu second organization, bu-hierarchy dogfood", "Per-org replay reference probe: seed-loader.ts:2021 adds organization_id to every reference probe when config.organizationId is set, including probes of a target with no tenant column (sys_user). The driver refuses that filter as an unresolvable column, the catch moves on, and a non-UUID user_id never resolves. Measured: a sys_business_unit_member seed row naming a better-auth user id was refused with 'User is required' plus 'Deferred reference unresolved after pass 2'. The unpinned load resolves the same id. No reach: no seed in the tree references sys_user (the only seeded sys_ object is sys_business_unit, examples/app-showcase/src/data/seed/index.ts:249), so there is no named producer. carrier: none · noted, not filed. dedupe words: per-org replay sys_user reference, organization_id probe untenanted target, resolveFromDatabase" ], "dedupe_words": [ "sys_business_unit_member organization_id NULL", "isPlatformObjectOutOfTenantAuditScope unclassified", "resolveSystemInsertOrganization early return", "C8 49 gated platform objects" ] }objectstack-fleet commented
on Oct 8, 2026 ContributorMore actionsStage 0 result: outcome (c). Re-blocked on C8 (#15212); claim released
domain:engine#1·session_01EUBvqtauTDmHi2ZgY759p2(os-litant), claim 6066790399 · 2026-10-08T19:11Z.Release: claim 6066790399 (
session_01EUBvqtauTDmHi2ZgY759p2, branchclaude/issue-14570-bu-member-organization, never pushed). By the claim's own outcome (c), the card returns topm:blockedand the assignee is removed.The dev's measurements are in os-dev-report 6067123924, on
maine36ee5351b, throughbootStack,singleandisolated:- Card row 2 (seed replay): done by C1. Every seed-loader shape stamps the organization.
- Card row 1 (REST / session): stamps, as before.
- Card row 3 (elevated system write): survives. A bare
isSysteminsert storesorganization_idNULL in both postures, with no refusal. The cause is the 49-object exemption C1 kept until C8 by ruling (resolveSystemInsertOrganization'sisPlatformObjectOutOfTenantAuditScopeearly return). ⛔ Not lifted here. - The card's dogfood instance (
showcase-bu-hierarchy-sharing.dogfood.test.ts:73–:74) still writes both memberships NULL.
Recorded:
- The remainder is on C8 as 6067156514, with the predicate, the sibling
sys_business_unitand the fixture's second-organization trap. - The body's
Blocked-by:line now reads#15212(it read#15195). - Existing rows stay C7's (feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211), per 5536484221.
Out of scope, one line each:
- The per-org replay reference probe adds
organization_idto a probe of an untenanted target (sys_user). No seed in the tree referencessys_user, so there is no producer. Carrier: none, noted. - The fixture's swallowed
findOnerefusal: carried on 6067156514.
Generated by Claude Code
Blocked-by: #15212
Filed as an out-of-scope finding while working #14547 (sharing rules with a business-unit recipient). Unassigned; recording, not claiming.
What was measured
sys_business_unit_membercarries anorganization_idcolumn —applySystemFieldsinjects it unconditionally for amanagedBy: 'platform'object, and the schema inpackages/platform-objects/src/identity/sys-business-unit-member.object.tsdeclares no tenancy opt-out. Whether a WRITER fills it depends entirely on the path:organization_id?execCtx.tenantIdintoDriverOptions, and the SQL driver'sinjectTenantOnInsertfills the injected columnpackages/metadata-protocol/src/seed-loader.tswithholds its single-orgfallbackOrgIdfrom everysys_/cloud_/ai_object, so a seeded membership lands org-less unless the replay pinned an organization or the record spells the column itselfsys_business_unit_memberisunclassifiedinPLATFORM_OBJECT_TENANCY(packages/objectql/src/tenancy/platform-object-tenancy.ts), soEngine.resolveSystemInsertOrganizationreturns early and stamps nothingdriver-memory/driver-mongodbThe repo's own dogfood fixture is an instance of row 3:
packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.tsinserts membership rows under a bareisSystemcontext and they land org-less, while thesys_business_unitrows beside them are explicitly stamped.Why it matters now
sys_business_unit_memberis the population an authorization path enumerates. The #14547 change screens it by organization for an org-stamped sharing rule — it has to, because the seeded unit id it now admits exists identically in every tenant, and an unscoped member read over a shared unit id is a cross-tenant over-grant. That screen is necessarily strict: a NULL organization on a membership row is not "platform-global", it is unknown tenancy, and a grant fails closed on it.The consequence is a residual, now-loud gap: a sharing rule carrying an organization whose business unit AND whose memberships were both produced by seed replay still expands to nobody.
SharingRuleService.expandRecipientwarns once per rule naming the rule and the unit, so it is no longer silent, but the repair is to stamp the membership rows — not to widen the screen.What the adjudication is
The
PLATFORM_OBJECT_TENANCYheader states that an object whose tenancy cannot be determined is LISTED for adjudication rather than guessed either way (2026-08-31 ruling, execution point 2: 「判不了的逐个列出回批呈裁,⛔ 不猜」). This is that listing for one object. The admission bar is a citable writer fact, and the REST path above is one — but promoting the entry is a maintainer call, not a developer's, and it changes behaviour (an org-less system insert on a walled posture becomes a loud refusal).Related, neither of them this object: #14096 asks whether the seed loader and the #8686 backfill should follow the per-object classification at all — the answer there decides row 2 of the table (⚠️ #14096 is now closed: ADR-0131 D9 answered it "neither", by removing the exemption rather than choosing). #13636 describes the third tenancy state this table's row 2/3 split keeps running into, where org-less is a property of the ROW rather than the object (⚠️ superseded under ADR-0131 §1.6 / C11).
Not proposed here
No backfill of existing rows. The 2026-08-31 ruling's execution point 3 governs that (⛔ never silently rewrite behaviour), and #14547 deliberately changed nothing about what any write touches.
Generated by Claude Code