Skip to content

fix(metadata-protocol): getMetaItemLayered gates the org read, bound after the canonical fold - #16034

Merged
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-14907-getmetaitemlayered-org-gate
Sep 5, 2026
Merged

fix(metadata-protocol): getMetaItemLayered gates the org read, bound after the canonical fold#16034
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-14907-getmetaitemlayered-org-gate

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #14907

getMetaItemLayered is the third /meta read verb, and the third instalment of the ungated-org-read series (#9454#14683 plural → #14770 singular). It applied no registry read gate of its own, so a caller could spend a raw active organization on a type with no per-org read channel and a pre-#6190 phantom org-scoped row became the overlay layer of the three-layer diagnostic — a false positive customization claim, rendered by the Studio "Code default vs Overlay vs Effective" tab as evidence.

The repair is a REORDER, not the siblings' one-liner

In both twins the orgId binding already sat after canonicalizeMetaRequestType, so the fix there was a substitution. Here it sat before the fold, and dropping the same expression in place would have gated on the RAW type — the #10340 defect: declaresOrgOverride tolerates the manifest plurals but not the URL-only spellings (translations / email_templates have no manifest key), so a raw segment splits one item across two partitions, addressed by spelling.

So the binding moved below the fold:

request = canonicalizeMetaRequestType(request);
// ... the gate's comment block, incl. the idempotence proof ...
const orgId = organizationIdForMetaRead(request.type, request.organizationId);

Site re-derived rather than taken from the card, which warned its own numbers drift: the ungated binding was at :7812, not the :7838 recorded. Both of the card's commands fired, each serving as the other's control — const orgId = request.organizationId; returned 1 hit before the change and 0 after; the gated-binding control returned 2 before (the landed twins) and 3 after.

⚠️ Two of the card's reachability claims measure FALSE

The card asked for this to be verified per site rather than inherited. It was, and it does not hold as written. Both errors are in the direction that ADDS callers to the gate, so the remedy shape is unchanged — but the idempotence proof had to be discharged over the real population.

Site Card Measured
runtime/src/domains/meta.ts:346 reachable, ungated ✅ confirmed
rest-server.ts /layers gates ✅ confirmed (at :3410, not :3172)
rest-server.ts second door "confirmed as stated" (gates) does not gate
five plugin-security sites "pass no organization at all" four of five do; one passes one

The second REST door. GET /meta/:type/:name/published passes publishedCtx.tenantId raw and then res.json(layered.overlay) — structurally the same shape as the runtime dispatcher branch the card identified, so it is a second reachable serving site, not a gated one. Its own comment argues the raw tenant "is right for a READ ... nothing that resolves today stops resolving", which is exactly the fail-open reasoning organizationIdForMetaRead's docblock exists to refute. That comment is why two seats recorded this door as gating; filed as #16032 (behaviour is repaired by this PR — only the comment is left, and correcting it would pull packages/rest into the diff).

The plugin-security site. permission-set-projection.ts:770 forwards evt.organizationId on a hard-coded type: 'permission', which is allowOrgOverride: false. It is fed by permission-set-overlay-discard.ts:244 (callerCtx?.tenantId, raw) and by the mutation-projector subscription. It moves, and moving it makes the projection read the partition boot hydration actually serves.

Three callers move, each one the defect. Every other caller is a no-op by f(t, undefined) === undefined or f(t, f(t, o)) === f(t, o); the proof is written out at the gate and pinned in §4/§5.

Tests — packages/metadata-protocol/src/get-meta-item-layered-org-read-gate.test.ts

18 cases. §0 pins the tier-A five and that object / permission are outside it; §1 is the phantom case; §2 is the must-not-deny-everyone direction; §3 is the section neither twin has — the after-fold ordering; §4/§5 are the idempotence legs over the complete accepted-spelling population; §6 pins the predicate both serving doors read.

Two ablations, disjoint failure sets — mutation proven on disk before measuring, restore proven byte-exact after, in one shell each:

Ablation Mutation Result
A gate removed (raw binding restored) 5 red — the four §1 phantom cases + §6. §3 green.
B the naive port: same expression, above the fold 2 redexactly §3. §1/§6 green.

Ablation B is the one that matters: it demonstrates the sibling verbs' one-liner would have shipped the #10340 partition split, and that this PR's pin catches it. Restores verified by blob hash (f5f22093… both sides) and an empty git diff HEAD.

⚠️ Ablation B's first two measurement attempts returned exit 99 (verify-lock queue timeout) — no reading, not a green; it was re-run to a real result, narrowed off the lock and declared below.

Fixture triage

One existing pin needed re-triage rather than a spelling tweak: protocol.metadata-store-outage.test.ts's "a failed ORG-scope read does not silently demote" used type: 'object' with an organization. Gated, that type issues no org-scope read at all, so the case would have passed vacuously against a method that had stopped doing the thing the case is about. Moved to view (tier-A), where the org arm is really taken. Scanned by the rule's consumption radius, not by package: the only other real-verb fixture carrying an organization is objectql's protocol-layered-get.test.ts, which already uses view — unaffected, and re-run green against the freshly built dist.

Verification (branch head f4ca1bceb)

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0. Its tsconfig is include: src/**/* with no test exclusion, so this covers the new test file rather than passing over it.
  • Derived affected set (every file in the package invoking this verb, by grep, plus the three sibling gate pins) — 9 files, 377 tests, exit 0.
  • Cross-package: objectql protocol-layered-get + protocol-meta2 files, 102 tests, exit 0, against a dist rebuilt at this head.
  • Targeted gates, all exit 0, verdict lines read from each gate's own output: check:nul-bytes, check:where-matcher, check:objectql-double-limit, check:engine-double-contract, check:test-source-alias, check:cross-package-test-inputs, check:filter-alias-parity, check:durability-log-level, check:type-source-resolution, check:published-files. The two ratchets report baseline key set verified against f7db8f4: no files added — the new test double needed no baseline entry.

Declared narrowing. The full 154-file package suite is not run here: the shared verify lock returned exit 99 on five separate acquisitions (box under heavy contention; holders and queue read from --status). Per the queue discipline the check was narrowed to the derived affected set above and the narrowing is declared rather than left implicit. CI runs the farm.

Tier — Clause-②: no, re-measured, not inherited

The claim's no was not taken on trust. api-surface/ covers only packages/spec, so the published surface here is files[] + types: built at head, swapped protocol.ts back to the merge base, rebuilt, diffed the built dist/index.d.ts, then restored byte-exact and rebuilt again.

DTS_HEAD_SHA = 137a1c5cf18002b726a2a5b1adeec40114a441f7
DTS_BASE_SHA = 137a1c5cf18002b726a2a5b1adeec40114a441f7   -> diff exit 0, 0 lines
restore: blob f5f22093… == HEAD blob, `git diff HEAD` empty, rebuild reproduces DTS_HEAD_SHA

Byte-identical. The diff changes a local const initializer and adds comments; no exported symbol, signature or type moves, and no request is newly accepted or rejected.

Notes


Generated by Claude Code

…he canonical fold

`getMetaItemLayered` — the third `/meta` read verb — applied no registry read
gate of its own, so a caller could spend a raw active organization on a type
with no per-org read channel and a pre-#6190 phantom org-scoped row became the
`overlay` layer of the three-layer diagnostic.

The binding moved BELOW `canonicalizeMetaRequestType` rather than being
replaced in place: it sat above the fold, so the sibling verbs' one-liner would
have gated on the RAW type, which #10340 measured splits one item across two
partitions for the URL-only spellings (`translations` / `email_templates`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…old ordering

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
The layered read now gates its organization, so an allowOrgOverride:false
type issues no org-scope read at all and the case would pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 10 documentable anchor(s).

24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 784595113caeb45bf3e0776a7550c5dbb240a9c5.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 784595113caeb45bf3e0776a7550c5dbb240a9c5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 006a4127460c14f43265af4dcc7506828fa80f4c — the merge of head f4ca1bceb48862eaa1492db8a46559fc23af4765 into base 784595113caeb45bf3e0776a7550c5dbb240a9c5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 006a4127460c14f43265af4dcc7506828fa80f4c && git checkout 006a4127460c14f43265af4dcc7506828fa80f4c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 784595113caeb45bf3e0776a7550c5dbb240a9c5 f4ca1bceb48862eaa1492db8a46559fc23af4765 && git checkout -B drift-repro 784595113caeb45bf3e0776a7550c5dbb240a9c5 && git merge --no-ff f4ca1bceb48862eaa1492db8a46559fc23af4765

node scripts/docs-audit/affected-docs.mjs --json 784595113caeb45bf3e0776a7550c5dbb240a9c5

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 784595113caeb45bf3e0776a7550c5dbb240a9c5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 5, 2026 21:00
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit e1d4f9e Sep 5, 2026
35 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-14907-getmetaitemlayered-org-gate branch September 5, 2026 21:52
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/l tests tooling

Projects

None yet

2 participants