Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/docs/permissions/system-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
53 changes: 34 additions & 19 deletions packages/rest/src/rest-server-meta-read-org-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends { type: string; name: string; organization_id: string | null }>(
Expand Down Expand Up @@ -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(
Expand Down
76 changes: 60 additions & 16 deletions packages/rest/src/rest-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading