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
6 changes: 5 additions & 1 deletion .changeset/repo-execute-elevated-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion content/docs/permissions/system-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
33 changes: 30 additions & 3 deletions packages/objectql/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
if (this.engine.executeAction) {
Expand Down
Loading