From d3400888c55ac8904ab5f85d5a00c273a25fbff6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 02:09:44 +0000 Subject: [PATCH 1/2] docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() PR #15051's "what widens" statement named only the static `readonly` strip that isSystem: true skips on ctx.api.object(x).update(y). That is true but materially incomplete: ObjectQL's registered security middleware (security-plugin.ts:1614-1616) reads isSystem as a TOTAL, unconditional bypass, so every find/insert/update/delete through this ctx.api also skips RLS scoping, field-level security, the CRUD/export checks, the ADR-0103 engine-owned write guard, the package-managed/system-row/curated-capability/ audience-anchor write gates, the referential-integrity check and the tenant-audit mute -- matching REST /actions and MCP run_action exactly, and documented in full at content/docs/permissions/system-context.mdx. Bounded on two sides: metadata-plane schema masking (object-schema-fls.ts) is a separate REST/GraphQL dispatch path this ctx.api surface never reaches, and plugin-sharing's insert/update materialisation skip was already retired by the 2026-08-31 ruling on #13533. Rewrites the changeset paragraph and the execute() docstring to agree; the PR body itself is corrected via the GitHub API in the same round (a merged PR's body can still be edited for the historical record). No implementation, census, ablation, gate, or changeset-level change -- prose only, per the review finding on #15051. Part of #13866 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 --- .changeset/repo-execute-elevated-context.md | 6 +++- packages/objectql/src/engine.ts | 33 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) 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/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) { From ba771e706b8ae3a001a7b4b6f56db7fc747fefed Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 02:22:55 +0000 Subject: [PATCH 2/2] fix(docs): re-anchor system-context census after docstring line shift The disclosure-completeness docstring insertion in engine.ts shifted every line after it by 26; check-system-context-census.mjs --fix re-anchored the one row that rotted off its line (the ScopedContext.isSystem getter, row 63). Pure line rot, no population or classification change (still 106 elevation read sites in 20 packages across 45 files, all anchored). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 --- 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 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 |