Found while verifying #6190 against origin/main (impact ② of that card). Filed separately per Prime Directive #10 — it is not flow-specific and it survives whichever way #6190's contract question is ruled, so it does not belong in #6190's diff.
The fact (measured, not inferred)
applyRegistryWriteThrough (packages/metadata-protocol/src/protocol.ts) states its own invariant in its TSDoc:
The non-object branch carries the same environmentId === undefined gate the read-side hydration carries: a project-scoped row must not be registered into a registry that unscoped (control-plane) callers share. The write must not be more permissive about that than the read is.
The gate is one line — if (this.environmentId !== undefined) return; — and it says nothing about organization_id. So on an unscoped kernel (environmentId === undefined), a per-org overlay write hydrates straight into the process-wide SchemaRegistry under the plain key.
Measured with the real ObjectStackProtocolImplementation over a stub engine, tracking registry.registerItem calls:
PROBE P3 rows = [{"type":"view","name":"org_grid","org":"org_a"}]
PROBE P3 registry write-through = [{"type":"view","name":"org_grid"}]
view is legitimately allowOrgOverride: true, so this is not a case of a type that should not have been writable — it is the designed per-org overlay leaking out of its org. flow behaves identically on the runtime-create tier (PROBE P1 registry write-through = [{"type":"flow","name":"org_sweep"}, ...]), which is how #6190's own impact ② was first seen.
This matters because loadMetaFromDb's filter carries the opposite rule as a comment, in the same file:
// ADR-0005 (revised 2026-05): hydrate only env-wide rows
// (organization_id IS NULL). Per-org overlays are loaded on
// demand by getMetaItem to avoid cross-org leakage into the
// process-wide SchemaRegistry.
Boot is careful about exactly the thing the write-through is not.
The read side has the same hole — so the TSDoc's framing is now wrong in a second way
getMetaItems merges orgRecords into overlays and then hydrates:
if (this.environmentId === undefined) {
for (const { data, packageId: recPkg } of overlays) {
this.hydrateOverlayIntoRegistry(request.type, data, recPkg);
}
}
overlays is built from envWideRecords plus orgRecords, so one org-scoped listing call (getMetaItems({ type, organizationId })) grafts that org's overlay bodies into the shared registry too. The write is therefore not "more permissive than the read" — both are equally permissive about org, and only the boot path holds the line. Whoever fixes this should fix both seams together, or the next listing call undoes the write-side fix.
Why it is observable, not theoretical
On an unscoped kernel getMetaItems starts from registry.listItems(type) and merges DB rows on top. Once org A's overlay body sits in the registry under the plain key, a listing for org B starts from org A's body — org B's own DB rows overwrite it only where the names collide, and where they do not, org A's item is simply in org B's list. Single-kernel is not a corner case here: per #5086's note in this same file, a host config with instantiated plugins boots new ObjectQLPlugin() with no environmentId, so the flagship showcase runs with environmentId === undefined.
Not a duplicate
Searched open issues/PRs across the three repos for applyRegistryWriteThrough, hydrateOverlayIntoRegistry, cross-org + SchemaRegistry, and the Chinese equivalents. Neighbours checked and distinct:
Refs
packages/metadata-protocol/src/protocol.ts — applyRegistryWriteThrough, hydrateOverlayIntoRegistry, the getMetaItems hydration loop, and loadMetaFromDb's filter comment; ADR-0005 §Tenant-customizable type whitelist; ADR-0010 §3.3; #4521 (the write-through's origin); #5086 (unscoped single kernels are a real end-user surface).
Found while verifying #6190 against
origin/main(impact ② of that card). Filed separately per Prime Directive #10 — it is notflow-specific and it survives whichever way #6190's contract question is ruled, so it does not belong in #6190's diff.The fact (measured, not inferred)
applyRegistryWriteThrough(packages/metadata-protocol/src/protocol.ts) states its own invariant in its TSDoc:The gate is one line —
if (this.environmentId !== undefined) return;— and it says nothing aboutorganization_id. So on an unscoped kernel (environmentId === undefined), a per-org overlay write hydrates straight into the process-wide SchemaRegistry under the plain key.Measured with the real
ObjectStackProtocolImplementationover a stub engine, trackingregistry.registerItemcalls:viewis legitimatelyallowOrgOverride: true, so this is not a case of a type that should not have been writable — it is the designed per-org overlay leaking out of its org.flowbehaves identically on the runtime-create tier (PROBE P1 registry write-through = [{"type":"flow","name":"org_sweep"}, ...]), which is how #6190's own impact ② was first seen.This matters because
loadMetaFromDb's filter carries the opposite rule as a comment, in the same file:Boot is careful about exactly the thing the write-through is not.
The read side has the same hole — so the TSDoc's framing is now wrong in a second way
getMetaItemsmergesorgRecordsintooverlaysand then hydrates:overlaysis built fromenvWideRecordsplusorgRecords, so one org-scoped listing call (getMetaItems({ type, organizationId })) grafts that org's overlay bodies into the shared registry too. The write is therefore not "more permissive than the read" — both are equally permissive about org, and only the boot path holds the line. Whoever fixes this should fix both seams together, or the next listing call undoes the write-side fix.Why it is observable, not theoretical
On an unscoped kernel
getMetaItemsstarts fromregistry.listItems(type)and merges DB rows on top. Once org A's overlay body sits in the registry under the plain key, a listing for org B starts from org A's body — org B's own DB rows overwrite it only where the names collide, and where they do not, org A's item is simply in org B's list. Single-kernel is not a corner case here: per #5086's note in this same file, a host config with instantiated plugins bootsnew ObjectQLPlugin()with no environmentId, so the flagship showcase runs withenvironmentId === undefined.Not a duplicate
Searched open issues/PRs across the three repos for
applyRegistryWriteThrough,hydrateOverlayIntoRegistry, cross-org + SchemaRegistry, and the Chinese equivalents. Neighbours checked and distinct:view/dashboard/report/translation/email_templateunder either ruling.allowOrgOverride: true. Orthogonal: this issue is about what happens to the rows of the types that legitimately are.Refs
packages/metadata-protocol/src/protocol.ts—applyRegistryWriteThrough,hydrateOverlayIntoRegistry, thegetMetaItemshydration loop, andloadMetaFromDb's filter comment; ADR-0005 §Tenant-customizable type whitelist; ADR-0010 §3.3; #4521 (the write-through's origin); #5086 (unscoped single kernels are a real end-user surface).