Skip to content

getMetaItemLayered is the third instalment of the ungated-org-read series — a raw active organization reaches its overlay layer, and its orgId is bound BEFORE the canonical type fold so the plural/singular one-liner does not port #14907

Description

@os-musk

Filed by the domain:engine execution seat while implementing #14770 (the singular getMetaItem door). Out of scope there and deliberately not fixed in that PR — see "Why it is not folded into #14770" below.

Ungraded and unrouted on purpose — no pm:* state, no domain:*, no assignee. Grading and routing are triage's.

The defect

The series #9454#14683 (plural getMetaItems) → #14770 (singular getMetaItem) is "a read door spends a raw active organization on a type that has no per-org read channel, so a pre-#6190 phantom org-scoped row is resurrected". getMetaItemLayered — the third /meta read verb — still applies no gate of its own.

Verified at origin/main 84b8190ae, packages/metadata-protocol/src/protocol.ts:

  • :7798async getMetaItemLayered(request: { … })
  • :7838const orgId = request.organizationId; ⇠ no gate
  • :7842request = canonicalizeMetaRequestType(request);

⚠️ Line numbers drift fast on this file — #14770 recorded three different readings of its own site inside two shifts. Re-derive:

git grep -n "const orgId = request.organizationId;" -- packages/metadata-protocol/src/protocol.ts

Control on the same file (expect the two GATED bindings, in getMetaItems and getMetaItem, after #14770 lands):

git grep -c "organizationIdForMetaRead(request.type, request.organizationId)" -- packages/metadata-protocol/src/protocol.ts

The live ungated caller

packages/runtime/src/domains/meta.ts:345 resolves the session organization and passes it straight through:

const organizationId = await deps.resolveActiveOrganizationId(_context);
const layered = await (protocol as any).getMetaItemLayered({
    type,
    name,
    ...(organizationId ? { organizationId } : {}),
});
if (layered?.overlay !== undefined && layered?.overlay !== null) {
    return { handled: true, response: deps.success(layered.overlay) };
}

type is generic here, so it is not confined to the org-overridable five. When it is a type the registry declares allowOrgOverride: false, a phantom org-scoped row becomes the overlay layer — and this branch returns that layer as the response, so the caller is told the item has a customization it does not have.

The REST /layers door at packages/rest/src/rest-server.ts:3172 does gate (organizationIdForMetaRead(canonicalMetaUrlType(req.params.type), layeredCtx?.tenantId)), and the plugin-security call sites pass no organization at all. So the runtime dispatcher site above is the reachable one.

⚠️ Direction differs from the other two verbs and should be measured before it is graded. On the plural verb a phantom can only ADD a row; on the singular verb it REPLACES the served document (#14770). Here the affected value is the overlay layer of a three-layer diagnostic whose whole purpose is to answer "what did this tenant customize" — so the harm is a false positive customization claim, which the Studio "Code default vs Overlay vs Effective" diff tab renders as evidence. That may grade differently from either twin.

⭐ Why it is not folded into #14770, and why it is not a copy of that fix

Two reasons, the second one substantive:

  1. The idempotence proof does not carry. The Where does the allowOrgOverride read gate belong for metadata sweeps that read MORE THAN ONE type per request? getMetaItems applies none of its own #14683 ruling made the callee-side gate conditional on proving no already-gating caller is double-scoped or wrongly denied, discharged per door over that door's own caller population. getMetaItemLayered has a different population — five plugin-security call sites, two REST doors, the runtime dispatcher — none of which getMetaItem (SINGULAR) has the same ungated-caller defect as getMetaItems and it is sharper — its overlay read is ?? PRECEDENCE, not a union, so a pre-#6190 phantom SHADOWS the live env-wide row and becomes the served document #14770's proof covers.

    ⚠️ Corrected 2026-09-03 (contract review of PR fix(metadata-protocol): gate getMetaItem's overlay read on the metadata registry #14908, advisory A2): this line first said six plugin-security sites. The actual non-test getMetaItemLayered( invocations there are fivepackaged-permission-set-lock-gate.ts:82 and permission-set-projection.ts:770, :938, :995, :1406. permission-set-overlay-discard.ts:243 only feature-detects (typeof protocol.getMetaItemLayered === 'function') and delegates to projectPermissionMutation, so it is not a call site. The two REST doors (rest-server.ts:3189, :7330) and the runtime dispatcher (runtime/src/domains/meta.ts:346) are confirmed as stated.

  2. ⚠️ The one-liner does not port. In both Where does the allowOrgOverride read gate belong for metadata sweeps that read MORE THAN ONE type per request? getMetaItems applies none of its own #14683 and getMetaItem (SINGULAR) has the same ungated-caller defect as getMetaItems and it is sharper — its overlay read is ?? PRECEDENCE, not a union, so a pre-#6190 phantom SHADOWS the live env-wide row and becomes the served document #14770 the fix is "replace the orgId binding with the gated call", and it is correct there because the binding sits after canonicalizeMetaRequestType. In getMetaItemLayered the binding is at :7838 and the fold is at :7842 — the binding comes first. Dropping the same expression in place would gate on the RAW type, which is exactly what /meta org scope is decided from the RAW url spelling: translations / email_templates read and write env-wide where their singular twin is org-scoped #10340 measured the cost of: declaresOrgOverride tolerates the manifest plurals but not the URL-only ones (translations / email_templates have no manifest key), so a raw segment splits one item across two partitions. The fix here needs the binding moved below the fold — a reorder, in a method whose code and overlay layers must answer for one namespace (that is what the fold's own comment at :7842 is about). Not mechanical, so it did not qualify for a bounded in-scope repair.

Dedup

MCP search_issues (repo-scoped REST is 403 from this container and gh is absent, so the one targeted MCP call was the channel) — "getMetaItemLayered ungated organization overlay layer raw active organization phantom org-scoped row runtime domains meta resolveActiveOrganizationId registry read gate singular layered door" → 10 results. Firing control: #14770 ranks first and #14683 is in the set. Distinguished:

Nothing open names getMetaItemLayered's ungated organization read.

Refs: #9454 · #14683 · #14770 (the singular twin, and the PR whose implementation surfaced this) · #6190 / #7018 (the phantom rows and the write-side predicate) · #10340 (the raw-vs-folded measurement) · #13753.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions