Measured while implementing #8323 (PR #8461). This PR unmasks the defect; it does not cause it — see "Provenance" below, which also says why restoring the global unique is not the remedy.
The mechanism
packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts upserts each capability by name:
constexisting=awaittryFind(ql,'sys_capability',{name: def.name},1);constrow=existing[0];tryFind runs under SYSTEM_CTX = { isSystem: true }, i.e. across organizations, and the query is limit: 1 with no ORDER BY. When two rows share a name — the platform's own (organization_id NULL) and one an admin authored inside an organization — the row this returns is arbitrary, and SQL guarantees nothing about which. It can differ between boots on the same database.
For a curated name the seeder then takes the update branch and writes the platform's label/description onto whichever row it got.
Measured
Against the seeder's own in-memory ql harness, which models isSystem faithfully (filters on where only, ignoring organization). Direction predicted before each run; all three matched.
| # | case | result |
|---|
| 1 | DERIVED name (approve_invoice) + org-authored row | ✅ safe — skippedAuthored: 1, org label preserved. The derivedNames.has(name) && row.managed_by !== 'platform' guard fires |
| 2 | CURATED name, platform row seeded first, org row added after | ⚠️safe in the harness ONLY — see the caveat below |
| 3 | CURATED name (manage_users), org row present, platform row not yet | ⛔ platformRowExists: false — the platform's own row is never seeded, and the org row's ORG CUSTOM LABEL is overwritten with Manage Users |
⚠️ The caveat on row 2 — please do not read this card as "2 of 3 safe"
Row 2 passes only because the fake returns rows in insertion order. The real query is find(..., { where: { name }, limit: 1 }) with no ORDER BY, and SQL provides no ordering guarantee there. On a real database, once two rows share a name the seeder's choice is arbitrary and may flip between boots — so row 2 is not a safe case, it is row 3 with an unobserved coin flip. Insertion order is a property of the test double, not a property of the system.
The concrete failure
platformRowExists: false. Two harms, the second worse:
- An organization's authored capability row is silently overwritten with the platform's copy at boot.
- A single tenant's row suppresses the platform's own definition installation-wide — the curated row is never created, in any bucket.
Scope of what was measured — verbatim, and please keep it prominent
I measured seeding behaviour only. I did not measure whether a missing platform capability row changes any authorization decision.
My reasoning that this is not a privilege issue — stated as my claim with its basis, not as a finding — is that sys_capability rows are definitions, while grants live in permission sets (systemPermissions) and requirements in requiredPermissions, both of which reference a capability by name rather than by row. That is why I expect a missing row to be a metadata/UX defect rather than an authz one. I did not test it, and I would rather the next person measure it than inherit my confidence.
Open question, explicitly unresolved:does a never-seeded curated capability break any downstream assumption? Candidates worth checking: validateCapabilityReferences (the authoring lint warns when requiredPermissions names a capability "registered nowhere"), Setup surfaces that list capabilities, and anything that resolves a capability row rather than the bare string.
Blast radius
Exactly the 8 curated names in PLATFORM_CAPABILITIES (packages/spec/src/security/capabilities.ts):
manage_users · manage_org_users · manage_metadata · manage_platform_settings · setup.access · setup.write · studio.access · manage_sharing
Derived names are guarded (row 1). Reaching it requires an org admin to author a sys_capability row whose name exactly matches one of those 8 — which is a supported action (managedBy: 'config'; ADR-0066 D1: the platform DEFINES, admins EXTEND in Setup).
sys_user_preference is unaffected — nothing seeds preferences by name across organizations.
Provenance — unmasked, not caused, and ⛔ not a reason to restore the global unique
The nondeterministic limit: 1 lookup predates#8461 and is unchanged by it. What #8461 changed is only that the two-row state became reachable: sys_capability.name was an installation-wide unique index, so an organization could not author a row named manage_users at all (it got 409) — the constraint made the ambiguity unreachable rather than correct.
⛔ Restoring the global unique is not the fix. That index is precisely the release-blocking cross-tenant existence oracle #8323 reports (an organization could read 409-vs-201 to enumerate values it cannot see, on the isolated posture sold as a hard wall). Reverting it to hide this defect would reopen a security hole to paper over a metadata one.
Direction
No contested intent here — the lookup is simply under-specified. Either or both:
- make the platform lookup deterministic (order the query, or select by identity rather than by name alone), and/or
- scope the platform seeder's lookup to
managed_by = 'platform' (and/or organization_id IS NULL), so it reconciles the row it owns and never an organization's.
The second matches the intent already expressed one branch down, where the derived half deliberately refuses to touch a row it does not own (#5876). The curated half simply never had the equivalent guard, because it never needed one.
Filed unassigned per the PM's sequencing. Not fixed in #8461 — deliberately, to keep a security release blocker narrow. Measured by session session_012WMpuAfA2KSdDjGF6tm1bH.
Generated by Claude Code
Measured while implementing #8323 (PR #8461). This PR unmasks the defect; it does not cause it — see "Provenance" below, which also says why restoring the global unique is not the remedy.
The mechanism
packages/plugins/plugin-security/src/bootstrap-system-capabilities.tsupserts each capability by name:tryFindruns underSYSTEM_CTX = { isSystem: true }, i.e. across organizations, and the query islimit: 1with noORDER BY. When two rows share aname— the platform's own (organization_idNULL) and one an admin authored inside an organization — the row this returns is arbitrary, and SQL guarantees nothing about which. It can differ between boots on the same database.For a curated name the seeder then takes the update branch and writes the platform's
label/descriptiononto whichever row it got.Measured
Against the seeder's own in-memory
qlharness, which modelsisSystemfaithfully (filters onwhereonly, ignoring organization). Direction predicted before each run; all three matched.approve_invoice) + org-authored rowskippedAuthored: 1, org label preserved. ThederivedNames.has(name) && row.managed_by !== 'platform'guard firesmanage_users), org row present, platform row not yetplatformRowExists: false— the platform's own row is never seeded, and the org row'sORG CUSTOM LABELis overwritten withManage UsersRow 2 passes only because the fake returns rows in insertion order. The real query is
find(..., { where: { name }, limit: 1 })with noORDER BY, and SQL provides no ordering guarantee there. On a real database, once two rows share a name the seeder's choice is arbitrary and may flip between boots — so row 2 is not a safe case, it is row 3 with an unobserved coin flip. Insertion order is a property of the test double, not a property of the system.The concrete failure
platformRowExists: false. Two harms, the second worse:Scope of what was measured — verbatim, and please keep it prominent
My reasoning that this is not a privilege issue — stated as my claim with its basis, not as a finding — is that
sys_capabilityrows are definitions, while grants live in permission sets (systemPermissions) and requirements inrequiredPermissions, both of which reference a capability by name rather than by row. That is why I expect a missing row to be a metadata/UX defect rather than an authz one. I did not test it, and I would rather the next person measure it than inherit my confidence.Open question, explicitly unresolved:does a never-seeded curated capability break any downstream assumption? Candidates worth checking:
validateCapabilityReferences(the authoring lint warns whenrequiredPermissionsnames a capability "registered nowhere"), Setup surfaces that list capabilities, and anything that resolves a capability row rather than the bare string.Blast radius
Exactly the 8 curated names in
PLATFORM_CAPABILITIES(packages/spec/src/security/capabilities.ts):manage_users·manage_org_users·manage_metadata·manage_platform_settings·setup.access·setup.write·studio.access·manage_sharingDerived names are guarded (row 1). Reaching it requires an org admin to author a
sys_capabilityrow whose name exactly matches one of those 8 — which is a supported action (managedBy: 'config'; ADR-0066 D1: the platform DEFINES, admins EXTEND in Setup).sys_user_preferenceis unaffected — nothing seeds preferences by name across organizations.Provenance — unmasked, not caused, and ⛔ not a reason to restore the global unique
The nondeterministic
limit: 1lookup predates#8461 and is unchanged by it. What #8461 changed is only that the two-row state became reachable:sys_capability.namewas an installation-wide unique index, so an organization could not author a row namedmanage_usersat all (it got409) — the constraint made the ambiguity unreachable rather than correct.⛔ Restoring the global unique is not the fix. That index is precisely the release-blocking cross-tenant existence oracle #8323 reports (an organization could read 409-vs-201 to enumerate values it cannot see, on the
isolatedposture sold as a hard wall). Reverting it to hide this defect would reopen a security hole to paper over a metadata one.Direction
No contested intent here — the lookup is simply under-specified. Either or both:
managed_by = 'platform'(and/ororganization_id IS NULL), so it reconciles the row it owns and never an organization's.The second matches the intent already expressed one branch down, where the derived half deliberately refuses to touch a row it does not own (
#5876). The curated half simply never had the equivalent guard, because it never needed one.Filed unassigned per the PM's sequencing. Not fixed in #8461 — deliberately, to keep a security release blocker narrow. Measured by session
session_012WMpuAfA2KSdDjGF6tm1bH.Generated by Claude Code