Skip to content

fix(metadata-protocol): an org-scoped DELETE no longer evicts the env-wide registry entry (#6780) - #6826

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-6780-org-gated-registry-heal
Aug 8, 2026
Merged

fix(metadata-protocol): an org-scoped DELETE no longer evicts the env-wide registry entry (#6780)#6826
os-zhuang merged 3 commits into
mainfrom
claude/issue-6780-org-gated-registry-heal

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #6780

The defect: one tenant's "reset" evicted the registry entry every tenant shares

restoreArtifactRegistryView — the three-tier heal that repairs the in-memory SchemaRegistry after an overlay-row delete — is (type, name)-addressed and org-blind. All three tiers write the plain key: removeRuntimeShadow drops it, the layer-2 baseline re-register rewrites it, removeOverlayEntry retires it. Per ADR-0005 there is exactly one plain-key entry per (type, name) in a process, and it belongs to the env-wide row — an org-scoped overlay never enters the registry at all since #6602.

deleteMetaItem called the heal on all three of its call sites without passing the delete's own scope (const orgId = request.organizationId ?? null fed the ROW delete only), so org A resetting its own customization reached in and cleared the entry every other org and the control plane read.

Premise re-measured on today's origin/main (5e247fd) — it holds

On an unscoped control-plane kernel — the shape #5086 measured the flagship showcase actually booting with:

after env save   : "Env grid"
after org save   : "Env grid"     # #6602 holding — the org row never enters the registry
delete receipt   : {"success":true,"reset":true,…}
after org DELETE : undefined      # the eviction
rows left        : [{"n":"shared_grid","org":null,"state":"active"}, …]

The env-wide row still sits in sys_metadata; only its registry entry is gone. While it is gone, direct registry readers answer as if the item does not exist — ADR-0110 D3's declaration gate, resolveRouteActionDeclaration, fail-closed assertObjectRegistered (404). One tenant's "reset my customization" degrades every other tenant's runtime until restart.

The self-heal branch is the cheaper door still: when org A has no overlay row at all, the delete receipt is {reset: false, "nothing to delete"} — and the heal ran anyway, evicting the entry just the same. A gate on the delete-ful branch alone would have left that door open.

The fix: the verdict moves inside the helper — organizationId as a required parameter (shape b)

The card offered two precedent shapes. (b) was chosen over (a) for a measured reason: there are four call sites, not the two the card names — deleteMetaItem has three (self-heal, post-repo.delete, legacy raw-engine path) and revertCommit one. PR #6807 had already gated the revert one; a call-site fix would have had to find the other three, and the legacy raw-engine path is exactly the kind a sweep misses. A required parameter makes the next caller answer at compile time; an optional one would default an omission back to "env-wide" and reinstate the hole. Same shape as #6602 / PR #6779's hydrateOverlayIntoRegistry on the register side.

#6807's call-site if (orgId === null) is now redundant-not-contradictory and was folded into the argument it passes — its pin still covers the batch path, and it goes red if the gate is ever removed (see below).

Register wide, retire narrow. The write-through's object carve-out stays un-org-gated and deliberately does NOT transfer to the delete side: it rests on assertObjectRegistered failing CLOSED, so a surplus entry degrades to "listable but rowless" and the next reload heals it, while a wrongly retired entry 404s data CRUD for every tenant. The two costs are asymmetric, so the two gates are too.

Unchanged: row-level delete behaviour (an org delete still removes the org row; that org's next read falls back to the env-wide body); the env-wide delete's full three-tier walk (#6687 tier 1 un-shadow, #5079 tier 3 retirement); and the kernel scope gate, which still guards re-registration only, because that is a fact about the kernel, not about this row.

Reverse verification (direction predicted before running)

With the organizationId refusal branch removed from restoreArtifactRegistryView, predicted 3 red / 4 green in the new block; measured 3 red / 4 green:

× …the card's sequence      → expected undefined to be defined
× …the SELF-HEAL branch     → expected undefined to be 'Env grid'
× …the runtime-SHADOW tier  → expected 'Artifact grid' to be 'Env grid'
✓ the ORG ROW is still removed · ✓ tier 1 · ✓ tier 3 · ✓ env-wide + org overlay

The first red is precisely the undefined the card measured, reproduced on demand. The four greens are not slack: the org-scoped ROW control asserts the delete itself and never reads the registry (a fake fix that skipped the delete entirely would fail there), and the three env-wide cases are the reason this heal exists — #6687's three-tier walk must not regress.

A fourth red lands in a file this PR does not edit, and it is the point of shape (b) rather than a surprise: PR #6807's own pin (protocol-commit-history.test.ts → "an ORG-scoped soft-remove leaves the env-wide registry entry alone") goes red too — expected null to be 'EnvWide' — because its call-site gate was folded into the argument it now passes. The gate moved; the coverage did not.

Tests

Extended in #6602's existing pin file (protocol-org-overlay-registry-gate.test.ts) rather than a parallel new one. That file's pre-existing #6602 — the delete chain needs no re-keying under this fix block also got an honest comment correction: its conclusion holds for a name only one org has touched, and fails when the same name also has an env-wide row — (type, name) cannot tell the two apart, so "no entry of its own" was never "no entry". This PR keeps the file's conclusion (no org-scoped registry keys) and adds the missing half (an org-scoped delete must not heal at all).

pnpm --filter @objectstack/metadata-protocol test   →  Test Files 63 passed (63) · Tests 740 passed (740)
pnpm --filter @objectstack/objectql test            →  Test Files 153 passed (153) · Tests 2663 passed (2663)
pnpm --filter @objectstack/{metadata-protocol,objectql} typecheck  →  exit 0
packages/runtime · meta-overlay-read-your-writes    →  Test Files 1 passed (1) · Tests 13 passed (13)
pnpm lint                                           →  exit 0

All 32 family gates inside .github/workflows/lint.yml's ESLint job were run individually in the foreground — every one PASS (incl. check:engine-double-contract, check:route-envelope, check:error-code-casing, check:meta-type-normalized, check:nul-bytes, check:adr-anchors).


Generated by Claude Code

@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 11:15pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 8, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 8, 2026 23:39
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 725c7b0 Aug 8, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6780-org-gated-registry-heal branch August 8, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An org-scoped metadata DELETE evicts the ENV-WIDE registry entry for every org — restoreArtifactRegistryView is (type, name)-addressed and org-blind

2 participants