Skip to content

Commit 4283b72

Browse files
os-muskclaude
andauthored
docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() (#15131)
* 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ebb0822 commit 4283b72

3 files changed

Lines changed: 36 additions & 5 deletions

File tree

.changeset/repo-execute-elevated-context.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ Before this change, the handler's `ctx` carried `params`, `userId`, `tenantId` a
88

99
`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.
1010

11-
**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.
11+
**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.
12+
13+
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.
14+
15+
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.

content/docs/permissions/system-context.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ a reader tracing where elevation travels needs them.
180180
| # | Site | Package | What it does |
181181
|:--|:---|:---|:---|
182182
| 62 | `objectql/src/engine.ts:3543` | objectql | Propagates `isSystem` into the hook session so hooks can tell engine self-writes from user writes |
183-
| 63 | `objectql/src/engine.ts:14496` | objectql | `ScopedContext.isSystem` getter — re-exposes the underlying execution context's flag |
183+
| 63 | `objectql/src/engine.ts:14523` | objectql | `ScopedContext.isSystem` getter — re-exposes the underlying execution context's flag |
184184
| 64 | `plugin-reports/src/report-service.ts:556` | plugin-reports | Threads the flag into the engine call that runs a report |
185185
| 65 | `body-runner.ts:279` | runtime | Rebuilds an `ExecutionContext` from a hook session, carrying the flag across |
186186

packages/objectql/src/engine.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14099,9 +14099,36 @@ export class ObjectRepository implements IScopedObjectRepository {
1409914099
* and test) found ZERO existing callers of this method anywhere — every
1410014100
* `ObjectRepository.execute()` / `ScopedRepo.execute()` hit in the tree was
1410114101
* prose describing the shape, never an invocation — so this widens what a
14102-
* FUTURE caller's write is accepted to do (the static `readonly` strip now
14103-
* skips this path exactly as it already skips REST `/actions` and MCP
14104-
* `run_action`) without changing any write anyone ships today.
14102+
* FUTURE caller's write is accepted to do, without changing any write
14103+
* anyone ships today.
14104+
*
14105+
* [Disclosure completeness] What widens is larger than the static
14106+
* `readonly` strip named above. `isSystem: true` on `this.context` is read
14107+
* by ObjectQL's registered security middleware as a TOTAL, unconditional
14108+
* bypass — `plugin-security/src/security-plugin.ts:1614-1616`, "System
14109+
* operations bypass security" / `return next()` ahead of every other gate
14110+
* in that middleware — so every `find`/`insert`/`update`/`delete` this
14111+
* `ctx.api` drives also skips RLS read scoping (`:4344`) and field-level
14112+
* security (`:4495`); the CRUD/export permission checks in the same
14113+
* middleware (`:1616`, `canExport` at `:4573`); the ADR-0103 engine-owned/
14114+
* append-only write guard (`system-write-guard.ts:96,120`, called at
14115+
* `security-plugin.ts:1736`); the package-managed / system-row /
14116+
* curated-capability-name / audience-anchor write gates
14117+
* (`security-plugin.ts:1690-1724`); the referential-integrity check
14118+
* (`:5892` in this file) and the tenant-audit mute (`:3773`) — in addition
14119+
* to the static `readonly`/runtime-owned strip on BOTH update paths
14120+
* (`:11290`, `:11473`) and the insert path (`:10025`), not the single site
14121+
* an earlier draft of this note implied. Exactly what REST `/actions` and
14122+
* MCP `run_action` already give an action body — see
14123+
* `content/docs/permissions/system-context.mdx` ("Elevation is total, and
14124+
* it is not granular") for the full catalog this bypass belongs to.
14125+
*
14126+
* Bounded on two sides: metadata-plane schema masking
14127+
* (`metadata-core/object-schema-fls.ts:228`) is a separate REST/GraphQL
14128+
* dispatch path this `ctx.api` surface never calls into; and
14129+
* `plugin-sharing/rule-hooks.ts`'s insert/update materialisation skip was
14130+
* already retired by the maintainer's 2026-08-31 ruling on #13533 (system
14131+
* and user writes materialise sharing grants identically today).
1410514132
*/
1410614133
async execute(actionName: string, params?: any): Promise<any> {
1410714134
if (this.engine.executeAction) {

0 commit comments

Comments
 (0)