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_member carries an organization_id column — applySystemFields injects it unconditionally for a managedBy: 'platform' object, and the schema in packages/platform-objects/src/identity/sys-business-unit-member.object.ts declares no tenancy opt-out. Whether a WRITER fills it depends entirely on the path:
| write path | stamps organization_id? | why |
|---|
| REST / session write | YES | the engine threads execCtx.tenantId into DriverOptions, and the SQL driver's injectTenantOnInsert fills the injected column |
| seed replay | NO | packages/metadata-protocol/src/seed-loader.ts withholds its single-org fallbackOrgId from every sys_ / cloud_ / ai_ object, so a seeded membership lands org-less unless the replay pinned an organization or the record spells the column itself |
| elevated (system-context) write | NO | sys_business_unit_member is unclassified in PLATFORM_OBJECT_TENANCY (packages/objectql/src/tenancy/platform-object-tenancy.ts), so Engine.resolveSystemInsertOrganization returns early and stamps nothing |
driver-memory / driver-mongodb | NO | neither implements a tenant column at all (both refuse to boot multi-tenant, which is what makes that safe) |
The repo's own dogfood fixture is an instance of row 3: packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts inserts membership rows under a bare isSystem context and they land org-less, while the sys_business_unit rows beside them are explicitly stamped.
Why it matters now
sys_business_unit_member is 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.expandRecipient warns 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_TENANCY header 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. #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.
Blocked-by:#14096
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
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. #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.
Blocked-by:#14096Not 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