You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Split out of #13753 by the domain:cli execution seat, because the answer lands in packages/metadata-protocol — another lane's package — and #13753 fences protocol.ts off. ⛔ Ungraded and unrouted on purpose: no pm:* state and no domain:*, so triage grades it. It lands in packages/metadata-protocol (and would touch getMetaItems), which the lane table puts in domain:engine.
Filed with the measurement already done, so grading does not need a dispatch first.
The measured fact this rests on
getMetaItems applies no registry gate of its own. Whatever organization it is handed is used for whatever type it is handed. The only organizationIdForMetaRead call inside @objectstack/metadata-protocol is the page read in protocol.ts; nothing on this path.
⇒ The scope of a metadata sweep is decided per type, by the caller. A request that carries one organizationId can therefore only be correct when it sweeps one type.
Two live doors sweep more than one:
door
what it sweeps
why one request-level organization cannot be right
GET /meta/diagnostics with no ?type=
the whole registry
one organization applied across types whose allowOrgOverride flags disagree
GET /meta/:type/:name/references
REFERENCE_SITES.byTarget → each matcher.fromType
req.params.type is the target; the organization is spent on the sources, so the target's own flag says nothing about the types actually read
The ?type= arm of /meta/diagnostics — the one case where one organization is the whole truth — is repaired in PR #14677. These two are not repairable from the call site, which is why they are here rather than there.
The harm, in both directions
Gating on the wrong type is wrong both ways round, and the two failures look nothing alike:
Target declares allowOrgOverride: true (view, dashboard, report, translation, email_template) ⇒ the organization is named for every source type, object / flow / app included. getMetaItems UNIONs queryByOrg(null) with queryByOrg(orgId), so naming an organization on a type with no per-org read channel resurrects the pre-org 作用域的 flow overlay 只在「本进程内发布后」绑定触发器,重启后静默失绑——冷启动两条读路径都把 organization_id 非空的行滤掉了 #6190 phantom org-scoped rows that boot hydration walks past. Those rows then appear inside a clearance shown before a destructive action.
⚠️One correction to #13753's premise, carried here because it changes the harm class. That card assumed naming the tenant unconditionally would hide an allowOrgOverride: false type's rows. That is true of /history — SysMetadataRepository.history() filters organization_id by strict equality — and it is not true on this path, where the union can only add rows. The registry-gated predicate is still the right instrument here; the reason is resurrection, not concealment.
Both consumers are in-tree and live: findReferencesToMeta backs the admin "Used by" panel, and the diagnostics sweep backs the Studio governance directory. An org-scoped view referencing a packaged object is the ordinary shape of an org overlay, so the false clearance is reachable on any deployment that uses overlays.
The options, as the reporting dev framed them
A — apply organizationIdForMetaRead(request.type, request.organizationId) INSIDE getMetaItems, and let every caller pass the session's raw active organization. One predicate governs read scope for all callers; the six gating call sites in rest-server.ts become redundant but idempotent.
B — apply the predicate per swept type inside each sweep: per t in getMetaDiagnostics, per matcher.fromType in findReferencesToMeta. getMetaItems untouched.
C — fan out at the REST call site: one getMetaDiagnostics call per org-overridable type plus one env-wide sweep, re-aggregating total / stats / scannedTypes / scannedItems in packages/rest. No protocol change.
D — leave both env-wide, document the limit on the doors, and accept the behaviour.
The dev recommended A, on these grounds, recorded here so the grading seat has them: the gate belongs where the type being read is known, and getMetaItems is the single seam every one of these reads already passes through, so read scope and write scope sit on one predicate and cannot drift for any caller. B fixes these two doors and leaves the rule per-caller — the shape that produced #9454, #9727, #13406 and #13753 in sequence, one door at a time. C is the only option that adds a mechanism, and it makes packages/rest a second owner of the sweep's arithmetic. D leaves a false clearance rendered to an operator at an irreversible action.
The cost of A, stated by the same dev rather than glossed: it touches a hot read path, it needs a proof of idempotence for the six call sites that already gate, and it needs pins for both sweeps. It is not a one-liner.
For the grading seat
The seat filing this has no opinion to record on A/B/C/D — it does not own packages/metadata-protocol. Two things worth weighing while grading:
Whether this is a queued fix with a recommended direction or a decision card. Nothing published moves under any option — no accept set, no wire shape, no authorable key — which argues queue; four live options with a hot-path cost argues decision. ⛔ The domain:cli seat does not pre-empt that.
Split out of #13753 by the
domain:cliexecution seat, because the answer lands inpackages/metadata-protocol— another lane's package — and #13753 fencesprotocol.tsoff. ⛔ Ungraded and unrouted on purpose: nopm:*state and nodomain:*, so triage grades it. It lands inpackages/metadata-protocol(and would touchgetMetaItems), which the lane table puts indomain:engine.Filed with the measurement already done, so grading does not need a dispatch first.
The measured fact this rests on
getMetaItemsapplies no registry gate of its own. Whatever organization it is handed is used for whatever type it is handed. The onlyorganizationIdForMetaReadcall inside@objectstack/metadata-protocolis thepageread inprotocol.ts; nothing on this path.⇒ The scope of a metadata sweep is decided per type, by the caller. A request that carries one
organizationIdcan therefore only be correct when it sweeps one type.Two live doors sweep more than one:
GET /meta/diagnosticswith no?type=allowOrgOverrideflags disagreeGET /meta/:type/:name/referencesREFERENCE_SITES.byTarget→ eachmatcher.fromTypereq.params.typeis the target; the organization is spent on the sources, so the target's own flag says nothing about the types actually readThe
?type=arm of/meta/diagnostics— the one case where one organization is the whole truth — is repaired in PR #14677. These two are not repairable from the call site, which is why they are here rather than there.The harm, in both directions
Gating on the wrong type is wrong both ways round, and the two failures look nothing alike:
allowOrgOverride: true(view,dashboard,report,translation,email_template) ⇒ the organization is named for every source type,object/flow/appincluded.getMetaItemsUNIONsqueryByOrg(null)withqueryByOrg(orgId), so naming an organization on a type with no per-org read channel resurrects the pre-org 作用域的 flow overlay 只在「本进程内发布后」绑定触发器,重启后静默失绑——冷启动两条读路径都把 organization_id 非空的行滤掉了 #6190 phantom org-scoped rows that boot hydration walks past. Those rows then appear inside a clearance shown before a destructive action.allowOrgOverride: false(object,flow,app,page, …) ⇒ nothing is named, so an org-scopedviewreferencing the object being deleted stays invisible and the admin "Used by" panel renders "Nothing in the metadata graph points at this item. Safe to delete." That is rest/meta: two more read doors (/meta/diagnostics,/meta/:type/:name/references) never forward the caller's organization — the "Used by" graph tells an operator an org-referenced item is safe to delete #13753's false clearance, on the most common delete there is.allowOrgOverride: falsetype's rows. That is true of/history—SysMetadataRepository.history()filtersorganization_idby strict equality — and it is not true on this path, where the union can only add rows. The registry-gated predicate is still the right instrument here; the reason is resurrection, not concealment.Both consumers are in-tree and live:
findReferencesToMetabacks the admin "Used by" panel, and the diagnostics sweep backs the Studio governance directory. An org-scoped view referencing a packaged object is the ordinary shape of an org overlay, so the false clearance is reachable on any deployment that uses overlays.The options, as the reporting dev framed them
A — apply
organizationIdForMetaRead(request.type, request.organizationId)INSIDEgetMetaItems, and let every caller pass the session's raw active organization. One predicate governs read scope for all callers; the six gating call sites inrest-server.tsbecome redundant but idempotent.B — apply the predicate per swept type inside each sweep: per
tingetMetaDiagnostics, permatcher.fromTypeinfindReferencesToMeta.getMetaItemsuntouched.C — fan out at the REST call site: one
getMetaDiagnosticscall per org-overridable type plus one env-wide sweep, re-aggregatingtotal/stats/scannedTypes/scannedItemsinpackages/rest. No protocol change.D — leave both env-wide, document the limit on the doors, and accept the behaviour.
The dev recommended A, on these grounds, recorded here so the grading seat has them: the gate belongs where the type being read is known, and
getMetaItemsis the single seam every one of these reads already passes through, so read scope and write scope sit on one predicate and cannot drift for any caller. B fixes these two doors and leaves the rule per-caller — the shape that produced #9454, #9727, #13406 and #13753 in sequence, one door at a time. C is the only option that adds a mechanism, and it makespackages/resta second owner of the sweep's arithmetic. D leaves a false clearance rendered to an operator at an irreversible action.The cost of A, stated by the same dev rather than glossed: it touches a hot read path, it needs a proof of idempotence for the six call sites that already gate, and it needs pins for both sweeps. It is not a one-liner.
For the grading seat
The seat filing this has no opinion to record on A/B/C/D — it does not own
packages/metadata-protocol. Two things worth weighing while grading:domain:cliseat does not pre-empt that./meta/diagnostics,/meta/:type/:name/references) never forward the caller's organization — the "Used by" graph tells an operator an org-referenced item is safe to delete #13753 stays open after PR rest/meta: state the caller's organization on the/meta/diagnostics?type=sweep #14677 lands (that PR is deliberatelyPart of, notFixes) and will be marked blocked on this card, so this one's grade decides when that card's remaining two halves can move.Refs
/meta/diagnostics,/meta/:type/:name/references) never forward the caller's organization — the "Used by" graph tells an operator an org-referenced item is safe to delete #13753 — the card this is split from; PR rest/meta: state the caller's organization on the/meta/diagnostics?type=sweep #14677 repairs its one call-side-reachable arm.view,dashboard) is accepted with a 200state:'active'receipt but served by no read door #9454, fix(rest): serve org-scoped metadata back through every /meta read door #9727, rest/meta: org-scoped metadata READ routes (/history, /diff, single-item dashboard overlay) read env-wide under isolated multi-org — the session org is not forwarded #13406 — the same rule rediscovered one door at a time.