Repository navigation
The audience-binding-suggestion reconciler writes with a tenant-less system context, so one organization-less row serves every tenant — the second half of the #8577 install-path dead end #8617
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 14, 2026 Blocked-by: #8577
Triage: lands in
packages/plugins/plugin-security(suggested-audience-bindings.ts—SYSTEM_CTXat line 43 verified on currentorigin/main; invocations insecurity-plugin.tsat boot and post-publish) ⇒domain:identity+security, type Bug, statepm:blockedon #8577, auto-adjudicated → Reading A (the suggestion surface is per-organization).Adjudication basis (veto window — flagged in the round report):
- The contract question is already decided by existing authority, not open: the object's own declared shape (
organization_idinjected, no tenancy opt-out, rows "resolved when a tenant admin confirms") and ADR-0090's model ("packages suggest, admins confirm" with the per-tenant confirm gate) both say per-organization. Reading B would need a tenancy opt-out, an explicitunique: 'global', and a brand-new per-tenant prompt surface — contradicting the declared shape and expanding architecture. Mechanical boundary test: A is declared = enforced restoration; accept set and public face unchanged ⇒ repair lane. - Four-prism block:
- Platform long-term coherence: A closes a declared/enforced split and aligns with ADR-0090 and ADR-0120 D3 tenancy; B multiplies special cases (opt-out + parallel prompt surface).
- Measured business pull: measured on a real engine — on any shared-runtime multi-org install, the second tenant's admin is never prompted, and the surface reports a suggestion resolved while the binding does not exist in that tenant's org.
- AI-agent error-resistance: declaration must equal enforcement; a tenant-blind reconciler behind a tenant-scoped declaration is exactly the silent-divergence class the platform refuses elsewhere.
- Startup scope discipline: A repairs an existing capability with no new surface; B declares-and-maintains new architecture.
- Ruled scope: the reconciler runs per organization with that organization's tenant-scoped context, and every read inside it (
findAnchorPositions,bindingExists, list/reconcile paths) is tenant-scoped. The two section-3 assertions insuggested-audience-bindings-install-path.test.tsmust be DELETED by the fix, per the card's own pin note. - Legacy-rows guard: pre-existing organization-less suggestion rows on deployed instances — the dev must measure and state their disposition in the PR. Reaping derived, reconciler-regenerable rows is inside this ruling; anything beyond that (touching non-derived data, or any tenancy opt-out on the object) triggers the fork-back clause: stop, flip this card to
needs-user-decision, never a silent re-decision. - Routing note: sibling Two MORE clean inheritors of the #8323 class the #8554 sweep missed: sys_notification_subscription and sys_audience_binding_suggestion — both measured live #8577 is
domain:metadata(index respelling on the object declaration); this card's named fix files are all inplugin-security⇒ identity per the anchoring rule.
Blocked-by rationale: #8577 (in flight) lands both the index respelling this fix requires (without it a tenant-scoped write is refused) and the test file this fix must edit. The unlock scan returns this card to the queue when #8577 closes.
Size/model suggestion: M,mode:subagent, opus.
Generated by Claude Code
- The contract question is already decided by existing authority, not open: the object's own declared shape (
Unlock sweep: blocker cleared.
pm:blocked→pm:queue.Declared blocker: objectstack#8577 — verified closed against the GitHub API on 2026-08-14. No other blocker is declared on this card.
⚠️ This card'sBlocked-by:line lives in a comment, not the body. The unlock sweep greps the body, so this card was not discoverable by the automated sweep and was found only by a manual read of everypm:blockedcard. Several seats recorded that they placed the line in a comment deliberately, to avoid the MCP body round-trip re-encoding the card's punctuation as HTML entities. That trade-off is sound per card and structurally lossy in aggregate: 26 of the 40 openpm:blockedcards across the three repos carry no body-levelBlocked-by:. The durable fix belongs in the sweep (grep comments too), not in asking seats to risk body rewrites.Authorization: maintainer directive (this session) —「先做 23 张 blocked 解锁 10 张 on-hold 按各自自述处理」· PM session
01JaVVMrSxt7Tgi1uwEuDtH7
Generated by Claude Code
上发版板 —— 加
target:v17。audience-binding-suggestion reconciler 用无租户的 system context 写入,于是一行 organization-less 记录服务了所有租户 —— 跨租户串数据,是 #8577 install-path 死路的后半段。
它不是凭据外泄,但同属第 ① 类:多租户部署里一个租户看到另一个租户的绑定建议,是「错数据」最直接的形态。
发版板判据是二元的:「不修它,当前 RC 能不能发?」,第 ① 类是「用户会撞上的已发布缺陷 —— 错数据、静默丢失、安全漏洞」。存储中的密钥可被读走、或被轮换流程销毁,都正落在这一类里。
v17 不是关闭的列车:17.0.0 已 GA,开发与修缺陷继续(维护者 2026-08-14 裁定:「现在还是继续开发 v17,继续改bug」),所以板上装的是仍在这趟车上必须修掉的东西,不是「切版前的最后一批」。
Authorization: maintainer directive (this session) —「先做 23 张 blocked 解锁 10 张 on-hold 按各自自述处理」· PM session
01JaVVMrSxt7Tgi1uwEuDtH7
Generated by Claude Code
Found while implementing #8577 (scoping
sys_audience_binding_suggestion's declared unique index per organization), by measuring the install path end to end through the real reconciler rather than stopping at the driver-level 409/201 oracle. Filed separately because #8577's scope is exactly the two index respellings; nothing here is fixed there.What #8577 fixes, and what it does not
#8577 makes the storage able to hold one suggestion row per organization:
(package_id, permission_set_name, anchor)stops being an installation-wide key. Measured with a tenant-threaded execution context on a realObjectQL+SqlDriverengine, driving the realsyncAudienceBindingSuggestions:But the shipped call path never threads a tenant.
packages/plugins/plugin-security/src/suggested-audience-bindings.tsreads and writes through a module-level constant:and
security-plugin.tsinvokes the reconciler with the bare engine at boot and after a package-doorpermissionpublish.listAudienceBindingSuggestionsgates on the caller's identity but then reconciles withSYSTEM_CTXtoo.Measured on a real engine (better-sqlite3,
OS_MULTI_ORG_ENABLED=true,OS_TENANCY_POSTURE=isolated){ isSystem: true }organization_idNULL{ isSystem: true, tenantId: X }organization_id= X{ isSystem: true }{ isSystem: true, tenantId: X }The last row is the platform's declared behaviour, not a surprise: the driver expands a tenant predicate to
organization_id = :tenant OR organization_id IS NULL(ADR-0120 D3's platform bucket).Running the reconciler exactly as shipped, twice, on the post-#8577 schema:
Identical before and after #8577. So on a shared-runtime multi-organization installation the surface holds one organization-less row that every tenant reads, and its consequences are:
confirmAudienceBindingSuggestioncreated the actualsys_position_permission_setbinding with the caller's context, i.e. in the confirming tenant's organization only. So for every other tenant the package's default permission set is still not bound, and the surface says it is.findAnchorPositionsresolvessys_positionby name under the same tenant-less context withlimit 1, so theeveryoneanchor a suggestion is checked against is whichever organization's row the driver returns first.bindingExiststherefore answers a question about some other tenant's binding.This is the same functional dead end #8577 describes — the second organization's admin is never prompted — reached by a second, independent road. Fixing the index is necessary (without it even a correctly tenant-scoped write is refused) but it is not sufficient on this deployment shape.
Suggested shape (not a decision — the tenancy question is the maintainer's)
Two readings, and they lead to different architectures, so this needs the ruling before code:
organization_idis injected with no tenancy opt-out). Then the reconciler must run per organization with that organization's context, and every read in it must be tenant-scoped. This is the reading Two MORE clean inheritors of the #8323 class the #8554 sweep missed: sys_notification_subscription and sys_audience_binding_suggestion — both measured live #8577's respelling assumes.unique: 'global'— and the per-tenant prompt has to live somewhere else, because one row cannot carry N tenants' decisions.B contradicts the object's declared shape and ADR-0090 D5/D9's per-tenant confirm gate, so A looks right, but the choice is a public-contract question about the object.
Pinned, not just described
packages/plugins/plugin-security/src/suggested-audience-bindings-install-path.test.ts(added by #8577) records today's behaviour in its section 3,the shipped SYSTEM_CTX call path is tenant-blind (recorded, not endorsed). Those two assertions are a record, not an endorsement: the fix for this issue must DELETE them, and if they still pass afterwards the fix did not work.Related
sys_position.nameis the third instance of the #8323 class: an admin-authored name on a tenant-scoped RBAC object carries an installation-wide unique index #8468 / Five more instances of the #8323 class: admin- and user-authored names on tenant-scoped objects still carry installation-wide unique indexes #8554 — the tenant-scoped declared unique index class.Filed unassigned, unlabeled for triage. Found by session
session_012WMpuAfA2KSdDjGF6tm1bH(dev seat for #8577).