diff --git a/.changeset/repo-execute-elevated-context.md b/.changeset/repo-execute-elevated-context.md index 67128a2df2..a8ce3a8e66 100644 --- a/.changeset/repo-execute-elevated-context.md +++ b/.changeset/repo-execute-elevated-context.md @@ -8,4 +8,8 @@ Before this change, the handler's `ctx` carried `params`, `userId`, `tenantId` a `ctx.api` is now a real `ScopedContext` bound to `{ ...callerContext, isSystem: true }` — the caller's own envelope, elevated — the same `sudo()`-shaped formula `buildActionExecutionContext` and `recomputeSummaries`'s `systemCtx` already use, so `userId`/`tenantId` still stamp the write and an open transaction still joins rather than escapes. `ctx.executionContext` carries the same elevated envelope, matching the REST/MCP shape exactly. -**What widens**: a `readonly: true` field a handler writes through `ctx.api.object(x).update(y)` when reached via `repo.execute()` now lands instead of being silently stripped, matching REST `/actions` and MCP `run_action`. A repo-wide census (production + test, `examples/` and `apps/` included) found no existing caller of `ObjectRepository.execute()` — every hit in the tree was prose describing the shape, never an invocation — so no shipped write changes behaviour. +**What widens**: `ctx.api` inside a `repo.execute()`-dispatched action handler now carries `isSystem: true`, and ObjectQL's registered security middleware reads that as a **total**, unconditional bypass (`plugin-security/src/security-plugin.ts:1614-1616`, "System operations bypass security" — `return next()` before every other gate in the middleware runs) — not only the static `readonly` strip named in earlier drafts of this note. Every `find`/`insert`/`update`/`delete` the handler drives through this `ctx.api` now also skips, in the same stroke: RLS read scoping (`security-plugin.ts:4344`) and field-level security (`:4495`); the CRUD/export permission checks in the same middleware (`:1616`, `canExport` at `:4573`); the ADR-0103 engine-owned/append-only write guard (`system-write-guard.ts:96,120`, called at `security-plugin.ts:1736`); the package-managed / system-row / curated-capability-name / audience-anchor write gates (`security-plugin.ts:1690-1724`); the referential-integrity check (`engine.ts:5892`) and the tenant-audit mute (`engine.ts:3773`); and the static `readonly`/runtime-owned strip on **both** UPDATE paths (`engine.ts:11290`, `:11473`) and the INSERT path (`:10025`), not the single call site named earlier. This matches the platform's own documented posture — `content/docs/permissions/system-context.mdx`: "Elevation is total, and it is not granular" — and REST `/actions` / MCP `run_action` already carry the identical exposure, so this widens an existing bypass to a third dispatch path rather than introducing a new one. + +Bounded on two sides: metadata-plane schema masking (`metadata-core/object-schema-fls.ts:228`) is a separate REST/GraphQL schema-serving dispatch path that this `ctx.api` surface (`find`/`insert`/`update`/`delete`/`count`/`aggregate`/`execute`) never calls into, so it is not reached here; and `plugin-sharing/rule-hooks.ts`'s insert/update `isSystem` materialisation skip was already retired by the maintainer's 2026-08-31 ruling on #13533 — a system write materialises sharing grants exactly as a user write does today, so nothing changes there either. + +A repo-wide census (production + test, `examples/` and `apps/` included) found no existing caller of `ObjectRepository.execute()` — every hit in the tree was prose describing the shape, never an invocation — so no shipped write changes behaviour today: the widening is total in kind, empty in measured blast radius. diff --git a/content/docs/permissions/system-context.mdx b/content/docs/permissions/system-context.mdx index 59a33e3f73..799a0d1907 100644 --- a/content/docs/permissions/system-context.mdx +++ b/content/docs/permissions/system-context.mdx @@ -180,7 +180,7 @@ a reader tracing where elevation travels needs them. | # | Site | Package | What it does | |:--|:---|:---|:---| | 62 | `objectql/src/engine.ts:3543` | objectql | Propagates `isSystem` into the hook session so hooks can tell engine self-writes from user writes | -| 63 | `objectql/src/engine.ts:14496` | objectql | `ScopedContext.isSystem` getter — re-exposes the underlying execution context's flag | +| 63 | `objectql/src/engine.ts:14523` | objectql | `ScopedContext.isSystem` getter — re-exposes the underlying execution context's flag | | 64 | `plugin-reports/src/report-service.ts:556` | plugin-reports | Threads the flag into the engine call that runs a report | | 65 | `body-runner.ts:279` | runtime | Rebuilds an `ExecutionContext` from a hook session, carrying the flag across | diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index 1a476fb09c..9e13e1778b 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -14099,9 +14099,36 @@ export class ObjectRepository implements IScopedObjectRepository { * and test) found ZERO existing callers of this method anywhere — every * `ObjectRepository.execute()` / `ScopedRepo.execute()` hit in the tree was * prose describing the shape, never an invocation — so this widens what a - * FUTURE caller's write is accepted to do (the static `readonly` strip now - * skips this path exactly as it already skips REST `/actions` and MCP - * `run_action`) without changing any write anyone ships today. + * FUTURE caller's write is accepted to do, without changing any write + * anyone ships today. + * + * [Disclosure completeness] What widens is larger than the static + * `readonly` strip named above. `isSystem: true` on `this.context` is read + * by ObjectQL's registered security middleware as a TOTAL, unconditional + * bypass — `plugin-security/src/security-plugin.ts:1614-1616`, "System + * operations bypass security" / `return next()` ahead of every other gate + * in that middleware — so every `find`/`insert`/`update`/`delete` this + * `ctx.api` drives also skips RLS read scoping (`:4344`) and field-level + * security (`:4495`); the CRUD/export permission checks in the same + * middleware (`:1616`, `canExport` at `:4573`); the ADR-0103 engine-owned/ + * append-only write guard (`system-write-guard.ts:96,120`, called at + * `security-plugin.ts:1736`); the package-managed / system-row / + * curated-capability-name / audience-anchor write gates + * (`security-plugin.ts:1690-1724`); the referential-integrity check + * (`:5892` in this file) and the tenant-audit mute (`:3773`) — in addition + * to the static `readonly`/runtime-owned strip on BOTH update paths + * (`:11290`, `:11473`) and the insert path (`:10025`), not the single site + * an earlier draft of this note implied. Exactly what REST `/actions` and + * MCP `run_action` already give an action body — see + * `content/docs/permissions/system-context.mdx` ("Elevation is total, and + * it is not granular") for the full catalog this bypass belongs to. + * + * Bounded on two sides: metadata-plane schema masking + * (`metadata-core/object-schema-fls.ts:228`) is a separate REST/GraphQL + * dispatch path this `ctx.api` surface never calls into; and + * `plugin-sharing/rule-hooks.ts`'s insert/update materialisation skip was + * already retired by the maintainer's 2026-08-31 ruling on #13533 (system + * and user writes materialise sharing grants identically today). */ async execute(actionName: string, params?: any): Promise { if (this.engine.executeAction) {