From d993d6c1e42bd4482026ae1e59d637d89ce91eba Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 00:05:51 +0000 Subject: [PATCH 1/2] docs(rest): the ?type= diagnostics comment describes today's tree, and names the inner gate it sits above MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The call-site comment beside `GET /meta/diagnostics`'s `?type=` arm asserted that `getMetaItems` applies no registry gate of its own, so the scope of a sweep is decided at the door, per type, by the caller. That was measured and true when it was written. #14683 put `organizationIdForMetaRead(request.type, request.organizationId)` inside `getMetaItems`, after the canonical fold, and the sentence has been false since. The design question the card opens — is the door-side predicate defence in depth, or is it now redundant? — is answered KEEP, from two measurements rather than a preference: * the VALUE is redundant. Both sites fold the identical string through the identical map (`canonicalMetaUrlType` at the door; the same function inside `getMetaItems` via `canonicalizeMetaRequestType` -> `canonicalMetaType`), so `f(t, f(t, o)) === f(t, o)`. Measured: swapping the door's predicate for a raw `diagnosticsCtx?.tenantId` leaves rest-server-meta-read-org-scope.test.ts 30/30 green. * but the two gates are not in the same TRUST DOMAIN. `getMetaDiagnostics` is not a member of `MetadataProtocol` — not required, not optional — which is why the door reaches it through `(p as any)` and 501s when it is absent. The inner gate therefore belongs to one implementation of an undeclared extension, while the door-side predicate holds for every `RestProtocol` a host can mount. Comments only; no observable behaviour moves, on either branch. The harness the card names is corrected in the same pass: its header carried the same falsified sentence and an ablation recipe ("swap the predicate for a raw ctx?.tenantId and that assertion, and only it, turns red") that no longer discriminates. Measured 30/30 green under that mutation, and 6 failed / 24 passed under the mutation that drops the organization -- which is what the file does still pin. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- .../rest-server-meta-read-org-scope.test.ts | 53 ++++++++----- packages/rest/src/rest-server.ts | 76 +++++++++++++++---- 2 files changed, 94 insertions(+), 35 deletions(-) diff --git a/packages/rest/src/rest-server-meta-read-org-scope.test.ts b/packages/rest/src/rest-server-meta-read-org-scope.test.ts index 5b8f48490e..cc71c4a222 100644 --- a/packages/rest/src/rest-server-meta-read-org-scope.test.ts +++ b/packages/rest/src/rest-server-meta-read-org-scope.test.ts @@ -567,28 +567,40 @@ describe('#13764 the history seams of this harness honour the org partition', () // // ⭐ WHY ONLY THE `?type=` ARM IS REPAIRED, and why the untyped sweep is // PINNED AS-IS rather than left unmentioned. `getMetaDiagnostics` reads each -// swept type through `getMetaItems({ type: t, organizationId })`, and -// `getMetaItems` applies NO registry gate of its own — the organization it is -// handed is used for whatever type it is handed. So the scope is per TYPE -// while the request carries ONE `organizationId`: +// swept type through `getMetaItems({ type: t, organizationId })`. +// +// ⚠️ [#14683, recorded by #15034] `getMetaItems` NOW APPLIES THE REGISTRY GATE +// ITSELF, after folding the request type. This header used to say it applied +// none and that the scope was therefore the caller's to decide per type; that +// sentence is FALSE on today's tree. What survives it is the arm split below, +// which is about how many types ONE `organizationId` is asked to cover: // // • `?type=` ⇒ `targetTypes` is exactly that one type, so // `organizationIdForMetaRead` over it IS the request's whole scope. Correct // by construction, and repaired here. // • no `?type=` ⇒ `targetTypes` is the whole registry, five -// `allowOrgOverride: true` types beside every other declared type. One org -// id cannot say "org-scoped for those five, env-wide for the rest", and -// `getMetaItems` UNIONS the named org's rows onto the env-wide ones — so a -// tenant named there would union pre-#6190 phantom rows (org-scoped rows on -// types with no per-org read channel, which boot hydration walks past) back -// into a governance report. The gap is reported on the card and pinned -// below so it cannot widen by accident in either direction. +// `allowOrgOverride: true` types beside every other declared type. The arm +// names no organization at all, so nothing is folded and nothing is +// unioned. ⚠️ The reason it stays that way is no longer "one org id cannot +// say org-scoped for those five, env-wide for the rest" — since #14683 the +// inner gate folds each `t` separately inside the sweep's own loop, so it +// could. It stays because closing it MOVES BEHAVIOUR and is somebody's +// decision on a card. The gap is pinned below so it cannot widen by +// accident in either direction. +// +// ── ⛔ WHAT THIS FILE NO LONGER DISCRIMINATES (#15034, MEASURED) ─────────── +// +// This header used to end: "Swap `organizationIdForMetaRead` for a raw +// `ctx?.tenantId` at the call site and that assertion, and only it, turns red." +// MEASURED on the merged tree, that ablation now leaves this file 30/30 GREEN +// — `getMetaItems`' own gate re-folds the raw tenant id, phantom control +// included. Same fate as #14677's ablation B, and for the same reason. // -// The controls are the load-bearing half. `?type=object` proves the predicate -// is the REGISTRY-GATED one: a phantom org-scoped `object` row is planted -// directly in the store — the write door cannot produce one, by #6190 — and -// the sweep must not see it. Swap `organizationIdForMetaRead` for a raw -// `ctx?.tenantId` at the call site and that assertion, and only it, turns red. +// ⇒ What this file DOES still discriminate is the organization being DROPPED: +// remove the `organizationId` the `?type=` arm passes and the six repair cases +// above turn red (measured: 6 failed / 24 passed). Read the two apart before +// citing this file as a pin on the door-side predicate — it pins that the arm +// still FOLDS, never that the fold happens at the door. /** Rows in the backing store for one `(type, name, org)` slot. */ function storedRowsFor( @@ -660,9 +672,12 @@ describe('#13753 GET /meta/diagnostics states the org partition on the ?type= ar // than written through the door. Rows like it exist in deployments // that ran before that ruling; boot hydration walks past them, so // they are dead, and a read door that named the org for every type - // would serve them again. PREDICTED DIRECTION: replace the - // predicate with `ctx?.tenantId` at the call site and the count - // below becomes 2. + // would serve them again. ⚠️ [#15034] PREDICTED DIRECTION, + // CORRECTED: replacing the predicate with `ctx?.tenantId` at the + // call site no longer moves this count — `getMetaItems`' own gate + // (#14683) re-folds it. What still drives it to 2 is a read door + // that reaches the store with the org unfolded, which is why the + // control stays. const written = await b.put(NON_OVERRIDABLE, 'accounts'); expect(written.status, 'the control never wrote').toBe(200); expect( diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 31d5b24f52..3c66d49dbb 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -4752,15 +4752,22 @@ export class RestServer { // [#13753] STATE THE ORG PARTITION — but only on the // arm where ONE organization is the whole truth. // - // `getMetaDiagnostics` reads each type through - // `getMetaItems({ type: t, organizationId })`, and - // `getMetaItems` applies NO registry gate of its own: - // whatever organization arrives is used for the type it - // is handed, overridable or not (measured — the only - // `organizationIdForMetaRead` call inside - // `metadata-protocol` is the `page` read in - // `protocol.ts`, nothing on this path). The scope is - // therefore decided HERE, per type, by the caller. + // `getMetaDiagnostics` reads each swept type through + // `getMetaItems({ type: t, organizationId })`. + // + // ⚠️ [#14683] `getMetaItems` NOW APPLIES THE REGISTRY GATE + // ITSELF — `organizationIdForMetaRead(request.type, + // request.organizationId)`, one statement after it folds the + // type through `canonicalizeMetaRequestType`. That is the + // ONE inner gate this call site now sits above; the sibling + // gate in the same file guards `getMetaItem` (the singular + // overlay read, #14908), which this arm never reaches. + // + // ⛔ Until #14683 this comment said `getMetaItems` applied NO + // registry gate of its own and the scope was therefore + // decided HERE, per type, by the caller. That sentence is + // FALSE on today's tree — do not reintroduce it, and do not + // reason from it. // // ⇒ The `?type=` arm is exactly one type // (`targetTypes = [request.type]`), so the predicate @@ -4769,6 +4776,31 @@ export class RestServer { // arm Studio's per-type directory drill-down uses, and // it is the arm repaired here. // + // ── WHY THE FOLD IS DOUBLED, AND STAYS DOUBLED (#15034) ── + // + // The VALUE is redundant, and measured to be. Both sites fold + // the identical string through the identical map — here + // `canonicalMetaUrlType`, inside `getMetaItems` the same + // function reached through `canonicalizeMetaRequestType` → + // `canonicalMetaType` — so `f(t, f(t, o)) === f(t, o)` and the + // inner application is the algebraic no-op. MEASURED: replace + // this predicate with a raw `diagnosticsCtx?.tenantId` and + // `rest-server-meta-read-org-scope.test.ts` stays 30/30 GREEN; + // the inner gate re-folds it, phantom control included. + // + // ⭐ It is KEPT anyway, and the reason is TRUST DOMAIN rather + // than value. `getMetaDiagnostics` is not a member of + // `MetadataProtocol` at all — not required, not optional — + // which is why it is reached through the `(p as any)` cast and + // why the 501 above exists. The inner gate therefore belongs + // to ONE implementation of an UNDECLARED extension, while this + // predicate sits on the REST boundary and holds for every + // `RestProtocol` a host can mount. Delete it and a REST door's + // tenant scope becomes a function of which kernel is mounted — + // and no pin can see that happen, because the harness boots the + // bundled implementation. Defence in depth, on a seam the type + // system does not cover. + // // ⛔ The UNTYPED sweep is deliberately left env-wide, // and this is a recorded gap rather than an oversight // (#13753 reports the shape). `targetTypes` is then the @@ -4783,13 +4815,25 @@ export class RestServer { // be read back INTO the governance report as `stats` // counts and diagnostic entries. A dashboard whose job // is reporting what is wrong would report rows that do - // not survive a restart. One org id cannot express a - // per-type scope, and inventing one at this call site - // (a fan-out per overridable type, plus a REST-side - // re-aggregation of `total`/`stats`/`scannedTypes`) - // would make this door a second owner of the sweep's - // arithmetic. The decision belongs where the type is - // known — see the card. + // not survive a restart. + // + // ⚠️ #14683 MOVED THIS ARGUMENT and the gap outlived it, so + // read the two apart. What used to hold the untyped arm shut + // was that one org id could not express a per-type scope from + // here without a fan-out per overridable type and a REST-side + // re-aggregation of `total`/`stats`/`scannedTypes`. That is no + // longer the obstacle: `getMetaDiagnostics` calls + // `getMetaItems` once per `t` INSIDE its own loop, and the + // inner gate folds each `t` separately, so a single + // `organizationId` handed to the untyped arm would already be + // narrowed per type — phantoms of non-overridable types + // included. ⛔ The gap nevertheless stays OPEN and stays + // PINNED: closing it moves observable behaviour and is a + // decision somebody makes on a card, not a side effect of a + // comment repair (#15034 files it). The pin that guards it is + // `the untyped sweep is still env-wide` in + // `rest-server-meta-read-org-scope.test.ts` — if it reddens, + // read that card before making it green. // // ⚠️ NOT a new org-resolution seam: `resolveExecCtx` is // memoised per request (WeakMap keyed by `req`), the From a8cfb760115d602fa447a871201e447593b8e422 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 00:37:03 +0000 Subject: [PATCH 2/2] docs(permissions): re-anchor the five rest-server.ts census citations after the +44 comment shift The `?type=` diagnostics comment added 44 lines above every `isSystem` read site in `packages/rest/src/rest-server.ts`, and row 50 of the system-context census anchors that file by line number, so all five citations rotted by exactly 44. Repaired with the gate's own `node scripts/check-system-context-census.mjs --fix`, which REFUSED ZERO files: per-file the page's distinct anchor count still equalled the lines the file offers, so the rewrite is a pure shift, not a guessed mapping. Verified rather than assumed: the full census JSON at the merge-base and at this head differs in exactly five `line` fields, each +44 -- same sites, same order, same receivers, same text counts. No elevation read site arrived or vanished. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- content/docs/permissions/system-context.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/permissions/system-context.mdx b/content/docs/permissions/system-context.mdx index 6f8740e51b..77d53a0121 100644 --- a/content/docs/permissions/system-context.mdx +++ b/content/docs/permissions/system-context.mdx @@ -158,7 +158,7 @@ The largest single consumer — **17 of the 106 sites**. |:--|:---|:---|:---|:---| | 48 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes | `action-execution.ts:138` | | 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` | -| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4972`, `:6386`, `:6634`, `:7065`, `:7258` | +| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:5016`, `:6430`, `:6678`, `:7109`, `:7302` | | 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` | | 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:422`, `external-datasource-routes.ts:302`, `package-routes.ts:97` | | 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |