docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() - #15131

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix
Sep 4, 2026
Merged

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute()#15131
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Part of #13866 — disclosure-completeness follow-up to #15051 (merged as a548550)

Why this exists

#15051 elevated ObjectRepository.execute()'s ctx.api to { ...callerContext, isSystem: true }, closing the third executeAction caller per #13866. Independent contract review returned exactly one finding on that PR, after it had already been merged by the maintainer: its "what widens" statement named only the static readonly strip, and that is true but materially incomplete. isSystem: true is a flag, not a behaviour — it is read by ObjectQL's registered security middleware as a total bypass, not a readonly-only one.

No implementation, census, ablation, gate, or changeset-level change. The elevation, the pins, the 5-case ablation and the patch level all stood on review — this PR only corrects the disclosure text in the three places that repeated the incomplete framing:

  1. .changeset/repo-execute-elevated-context.md
  2. the execute() docstring in packages/objectql/src/engine.ts
  3. this comment thread + a body edit on fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture #15051 itself (a merged PR's body can still be edited for the historical record — done in the same round as this PR)

A fourth file changed for mechanical reasons only: content/docs/permissions/system-context.mdx — the docstring insertion shifted every later line in engine.ts by 26, rotting one anchor (check-system-context-census.mjs's own --fix, pure line rot, no population/classification change: still 106 elevation-read sites in 20 packages across 45 files, all anchored).

The corrected "what widens" statement

ctx.api inside a repo.execute()-dispatched action handler 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() ahead of every other gate in that middleware. Every find/insert/update/delete the handler drives through this ctx.api therefore also skips, in the same stroke as the readonly strip:

BehaviourAnchorConfirmed
Security middleware total short-circuit (the gate itself)security-plugin.ts:1614-1616
RLS read scoping (getReadFilter)security-plugin.ts:4344
Field-level securitysecurity-plugin.ts:4495
CRUD permission checks (same middleware)security-plugin.ts:1616
Export permission (canExport)security-plugin.ts:4573
ADR-0103 engine-owned/append-only write guardsystem-write-guard.ts:96,120, called at security-plugin.ts:1736
package-managed / system-row / curated-capability / audience-anchor write gatessecurity-plugin.ts:1690-1724
Static readonly strip — UPDATE, single rowengine.ts:11290
Static readonly strip — UPDATE, bulkengine.ts:11473
Static readonly/runtime-owned strip — INSERT (stripRuntimeOwnedFields, covers autonumber AND readonly together)engine.ts:10025
Referential-integrity check (assertReferencesResolve)engine.ts:5892
Tenant-audit muteengine.ts:3773
Hook-session isSystem propagation (carries the flag onward, not a gate)engine.ts:3543 (buildSession)
Audit attribution label ('system' vs 'anonymous', not a gate)engine.ts:12014

Every row re-derived independently by reading the cited line on this branch (not copied from the review) — see the terminal report for the full method.

Bounded on two sides, also re-verified independently:

  • Metadata-plane schema masking (metadata-core/object-schema-fls.ts:228) is a separate REST/GraphQL schema-serving dispatch path (GET /meta/object/:name and friends) that this ctx.api surface (find/insert/update/delete/count/aggregate/execute) never calls into — confirmed by reading the file's own header (@objectstack/metadata-core depends on spec+zod only; its two consumers are @objectstack/rest and @objectstack/runtime's schema-serving exits, not the engine's registered middleware chain). isSystemdoes bypass this elsewhere in the platform (content/docs/permissions/system-context.mdx row 7) — just not reachable from this PR's widening.
  • plugin-sharing/rule-hooks.ts's insert/update materialisation skip was already retired by the maintainer's 2026-08-31 ruling on 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 — confirmed by reading the file: the skip and its announcing INFO notice are both deleted, and the surviving isSystem check at rule-hooks.ts:292 is a different skip (delete-side revocation timing, delegated to record-share-cascade.ts, not a silencer) per the file's own comment. System and user writes materialise sharing grants identically today.
  • Zero measured blast radius today: independently re-ran the chained-accessor grep (.object(x).execute() across packages/, examples/, apps/ — zero hits outside this PR's own pin test (engine-repo-execute-elevation.test.ts), against a firing positive control (.object(x).update( → 195 hits, .insert( → 49 hits) confirming the grep methodology isn't blind. So the widening is total in kind, empty in practice, today.

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 #15051 extends an existing bypass to a third dispatch path rather than introducing a new one.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths — merge-base derivation against origin/main), re-derived after the anchor-fix commit: 73 commands on the final 3-file diff. All passed except two, both pre-existing "build the whole monorepo first" prerequisites unrelated to this diff (same shape #15051 itself reported):

  • check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every workspace package's dist/)
  • @objectstack/spec check:skill-examples → needs @objectstack/client-react built (unrelated to packages/objectql/docs)

node scripts/pm/check-governed-merges.mjs --test .changeset/repo-execute-elevated-context.md content/docs/permissions/system-context.mdx packages/objectql/src/engine.tsNOT governed (0 of 3 paths hit GOVERNED_SURFACES).

The PR's own 5 pins (packages/objectql/src/engine-repo-execute-elevation.test.ts) and the package typecheck were re-run on this branch to confirm the text-only edit disturbed neither: 5 passed (5), typecheck clean, both on final head ba771e706.

Scope note

No code behaviour changes here — only prose (a changeset paragraph, a docstring, one doc anchor). Left as draft; not enqueued, not auto-merge-armed. needs:contract-review was already cleared on #15051 when it merged — flagging in the report that this seat could not mechanically re-apply a label from here (no label-write tool available) and leaving that call to the PM/reviewing seat.


Generated by Claude Code

…ass 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
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 491493b45069b9ae845b2634d29a8e876933a643 — the merge of head ba771e706b8ae3a001a7b4b6f56db7fc747fefed into base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 491493b45069b9ae845b2634d29a8e876933a643 && git checkout 491493b45069b9ae845b2634d29a8e876933a643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e ba771e706b8ae3a001a7b4b6f56db7fc747fefed && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff ba771e706b8ae3a001a7b4b6f56db7fc747fefed
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@os-muskos-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: Check Changeset resolved, and the one remaining action is not mine to take

The red gate, read rather than guessed.Check Changeset was failure (run 100890560827). Its own job log names exactly two routes: apply skip-changeset if the PR releases nothing, otherwise run pnpm changeset and name the packages — and states that an empty-frontmatter changeset is not a third option.

Route taken: skip-changeset. This PR modifies.changeset/repo-execute-elevated-context.md; it does not add one. The release it corrects is already declared on main by #15051, and this PR adds no package bump of its own — it changes what that release's note says, not what it releases. Adding a second changeset would declare a release that does not exist.

Label write — read-modify-write with compare read-back, per protocol:

  • before: documentation, size/s, tooling
  • written: the same three plus skip-changeset
  • read back live: documentation, size/s, tooling, skip-changeset — matches the union exactly, nothing stripped by a concurrent writer.

The label event re-fired the workflow: new Check Changeset run 100892547790skipped (02:43:32Z). The earlier failure run remains visible in the check-run list as a superseded run; the latest run per check name is what branch protection reads.

One claim in this PR independently re-verified, not taken from the report. The only non-prose edit here is the shifted census anchor. Reading packages/objectql/src/engine.ts at head ba771e706 line 14523: get isSystem() { return this.executionContext.isSystem; } — the ScopedContext.isSystem getter, exactly what the table row claims. The --fix re-derivation is correct. (Note for the record: the PR body's prose says the docstring shifted later lines "by 26"; the actual shift is 27, 14496 → 14523. The anchor written to the doc is right; only the prose describing the offset is off by one.)

State now: 35 of 37 check runs green or skipped; Test Core (1/6) and Lint & Repo Gates still in progress at the time of writing. Not governed (check-governed-merges.mjs --test → 0 of 3 paths). Clause-② no — prose only: no new exported symbol, no new key on a published payload.

⛔ What this seat cannot do, stated plainly rather than left as silence. The PR is still a draft. GitHub exposes draft→ready only as the GraphQL mutation markPullRequestReadyForReview; REST PATCH on a pull request has no draft field, and the MCP update tool performs a GraphQL lookup before its REST write. This account's GraphQL quota is exhausted right now (REST calls continue to succeed — the split is REST vs GraphQL, not read vs write), so the flip is structurally unavailable from here, not merely worth retrying. Enabling auto-merge is blocked by the same two facts: it is a GraphQL mutation, and it refuses a draft PR regardless.

So one human action remains — mark this PR ready for review. After that the ordinary queue path applies; no other gate is outstanding. Flagging it here rather than reporting the PR as landed, because it is not.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 03:27
@os-musk
os-musk enabled auto-merge September 4, 2026 03:27
@os-musk
os-musk added this pull request to the merge queueSep 4, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Superseded: the handoff above is no longer needed

The previous comment ended by asking a human to mark this PR ready for review, because the GraphQL bucket was exhausted and markPullRequestReadyForReview is the only door to that flip. The quota recovered a few minutes later and this seat did it itself — so ignore that ask; nothing is waiting on a person here.

Sequence, so the record is not just an assertion:

  1. Check Changeset re-fired on the skip-changeset label event → run 100892547790, skipped.
  2. All 39 check runs now read completed, zero in progress. Every conclusion is success or skipped. The one failure in the list is the superseded Check Changeset run 100890560827 from 02:33, whose successor is the skipped run above — branch protection reads the latest run per check name, and the last two stragglers went green at 02:46:59 (Test Core (1/6)) and 02:50:11 (Lint & Repo Gates).
  3. Draft → ready: done.
  4. Auto-merge: armed.

One caveat stated rather than dressed up as certainty. The auto-merge receipt came back in the empty form, (method: , enabled at ). That form carries no established meaning — both the filled and the empty form have preceded successful landings here, and an earlier claim in this seat's notes that the empty form meant "already queued" was measured false and withdrawn. There is a reliable positive reading (a merge_group workflow run on a gh-readonly-queue/main/pr-15131-… branch) but no cheap negative one, so arming is not proof of landing. At the time of writing this PR had not yet appeared in the queue; five other entries were ahead of it.

That matters because of how the last one failed: #15008 sat armed for seven hours without landing, because it had gone mergeable_state: "dirty" and auto-merge does not fire on a conflicted PR and emits no event. If this PR is still open some hours from now, that is the first thing to check — absence of an event is not evidence of progress.


Generated by Claude Code

Merged via the queue into main with commit 4283b72Sep 4, 2026
39 of 40 checks passed
@os-musk
os-musk deleted the claude/issue-13866-disclosure-fix branch September 4, 2026 03:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() - #15131

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix
Sep 4, 2026
Merged

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute()#15131
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Part of #13866 — disclosure-completeness follow-up to #15051 (merged as a548550)

Why this exists

#15051 elevated ObjectRepository.execute()'s ctx.api to { ...callerContext, isSystem: true }, closing the third executeAction caller per #13866. Independent contract review returned exactly one finding on that PR, after it had already been merged by the maintainer: its "what widens" statement named only the static readonly strip, and that is true but materially incomplete. isSystem: true is a flag, not a behaviour — it is read by ObjectQL's registered security middleware as a total bypass, not a readonly-only one.

No implementation, census, ablation, gate, or changeset-level change. The elevation, the pins, the 5-case ablation and the patch level all stood on review — this PR only corrects the disclosure text in the three places that repeated the incomplete framing:

  1. .changeset/repo-execute-elevated-context.md
  2. the execute() docstring in packages/objectql/src/engine.ts
  3. this comment thread + a body edit on fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture #15051 itself (a merged PR's body can still be edited for the historical record — done in the same round as this PR)

A fourth file changed for mechanical reasons only: content/docs/permissions/system-context.mdx — the docstring insertion shifted every later line in engine.ts by 26, rotting one anchor (check-system-context-census.mjs's own --fix, pure line rot, no population/classification change: still 106 elevation-read sites in 20 packages across 45 files, all anchored).

The corrected "what widens" statement

ctx.api inside a repo.execute()-dispatched action handler 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() ahead of every other gate in that middleware. Every find/insert/update/delete the handler drives through this ctx.api therefore also skips, in the same stroke as the readonly strip:

BehaviourAnchorConfirmed
Security middleware total short-circuit (the gate itself)security-plugin.ts:1614-1616
RLS read scoping (getReadFilter)security-plugin.ts:4344
Field-level securitysecurity-plugin.ts:4495
CRUD permission checks (same middleware)security-plugin.ts:1616
Export permission (canExport)security-plugin.ts:4573
ADR-0103 engine-owned/append-only write guardsystem-write-guard.ts:96,120, called at security-plugin.ts:1736
package-managed / system-row / curated-capability / audience-anchor write gatessecurity-plugin.ts:1690-1724
Static readonly strip — UPDATE, single rowengine.ts:11290
Static readonly strip — UPDATE, bulkengine.ts:11473
Static readonly/runtime-owned strip — INSERT (stripRuntimeOwnedFields, covers autonumber AND readonly together)engine.ts:10025
Referential-integrity check (assertReferencesResolve)engine.ts:5892
Tenant-audit muteengine.ts:3773
Hook-session isSystem propagation (carries the flag onward, not a gate)engine.ts:3543 (buildSession)
Audit attribution label ('system' vs 'anonymous', not a gate)engine.ts:12014

Every row re-derived independently by reading the cited line on this branch (not copied from the review) — see the terminal report for the full method.

Bounded on two sides, also re-verified independently:

  • Metadata-plane schema masking (metadata-core/object-schema-fls.ts:228) is a separate REST/GraphQL schema-serving dispatch path (GET /meta/object/:name and friends) that this ctx.api surface (find/insert/update/delete/count/aggregate/execute) never calls into — confirmed by reading the file's own header (@objectstack/metadata-core depends on spec+zod only; its two consumers are @objectstack/rest and @objectstack/runtime's schema-serving exits, not the engine's registered middleware chain). isSystemdoes bypass this elsewhere in the platform (content/docs/permissions/system-context.mdx row 7) — just not reachable from this PR's widening.
  • plugin-sharing/rule-hooks.ts's insert/update materialisation skip was already retired by the maintainer's 2026-08-31 ruling on 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 — confirmed by reading the file: the skip and its announcing INFO notice are both deleted, and the surviving isSystem check at rule-hooks.ts:292 is a different skip (delete-side revocation timing, delegated to record-share-cascade.ts, not a silencer) per the file's own comment. System and user writes materialise sharing grants identically today.
  • Zero measured blast radius today: independently re-ran the chained-accessor grep (.object(x).execute() across packages/, examples/, apps/ — zero hits outside this PR's own pin test (engine-repo-execute-elevation.test.ts), against a firing positive control (.object(x).update( → 195 hits, .insert( → 49 hits) confirming the grep methodology isn't blind. So the widening is total in kind, empty in practice, today.

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 #15051 extends an existing bypass to a third dispatch path rather than introducing a new one.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths — merge-base derivation against origin/main), re-derived after the anchor-fix commit: 73 commands on the final 3-file diff. All passed except two, both pre-existing "build the whole monorepo first" prerequisites unrelated to this diff (same shape #15051 itself reported):

  • check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every workspace package's dist/)
  • @objectstack/spec check:skill-examples → needs @objectstack/client-react built (unrelated to packages/objectql/docs)

node scripts/pm/check-governed-merges.mjs --test .changeset/repo-execute-elevated-context.md content/docs/permissions/system-context.mdx packages/objectql/src/engine.tsNOT governed (0 of 3 paths hit GOVERNED_SURFACES).

The PR's own 5 pins (packages/objectql/src/engine-repo-execute-elevation.test.ts) and the package typecheck were re-run on this branch to confirm the text-only edit disturbed neither: 5 passed (5), typecheck clean, both on final head ba771e706.

Scope note

No code behaviour changes here — only prose (a changeset paragraph, a docstring, one doc anchor). Left as draft; not enqueued, not auto-merge-armed. needs:contract-review was already cleared on #15051 when it merged — flagging in the report that this seat could not mechanically re-apply a label from here (no label-write tool available) and leaving that call to the PM/reviewing seat.


Generated by Claude Code

…ass 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
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 491493b45069b9ae845b2634d29a8e876933a643 — the merge of head ba771e706b8ae3a001a7b4b6f56db7fc747fefed into base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 491493b45069b9ae845b2634d29a8e876933a643 && git checkout 491493b45069b9ae845b2634d29a8e876933a643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e ba771e706b8ae3a001a7b4b6f56db7fc747fefed && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff ba771e706b8ae3a001a7b4b6f56db7fc747fefed
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@os-muskos-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: Check Changeset resolved, and the one remaining action is not mine to take

The red gate, read rather than guessed.Check Changeset was failure (run 100890560827). Its own job log names exactly two routes: apply skip-changeset if the PR releases nothing, otherwise run pnpm changeset and name the packages — and states that an empty-frontmatter changeset is not a third option.

Route taken: skip-changeset. This PR modifies.changeset/repo-execute-elevated-context.md; it does not add one. The release it corrects is already declared on main by #15051, and this PR adds no package bump of its own — it changes what that release's note says, not what it releases. Adding a second changeset would declare a release that does not exist.

Label write — read-modify-write with compare read-back, per protocol:

  • before: documentation, size/s, tooling
  • written: the same three plus skip-changeset
  • read back live: documentation, size/s, tooling, skip-changeset — matches the union exactly, nothing stripped by a concurrent writer.

The label event re-fired the workflow: new Check Changeset run 100892547790skipped (02:43:32Z). The earlier failure run remains visible in the check-run list as a superseded run; the latest run per check name is what branch protection reads.

One claim in this PR independently re-verified, not taken from the report. The only non-prose edit here is the shifted census anchor. Reading packages/objectql/src/engine.ts at head ba771e706 line 14523: get isSystem() { return this.executionContext.isSystem; } — the ScopedContext.isSystem getter, exactly what the table row claims. The --fix re-derivation is correct. (Note for the record: the PR body's prose says the docstring shifted later lines "by 26"; the actual shift is 27, 14496 → 14523. The anchor written to the doc is right; only the prose describing the offset is off by one.)

State now: 35 of 37 check runs green or skipped; Test Core (1/6) and Lint & Repo Gates still in progress at the time of writing. Not governed (check-governed-merges.mjs --test → 0 of 3 paths). Clause-② no — prose only: no new exported symbol, no new key on a published payload.

⛔ What this seat cannot do, stated plainly rather than left as silence. The PR is still a draft. GitHub exposes draft→ready only as the GraphQL mutation markPullRequestReadyForReview; REST PATCH on a pull request has no draft field, and the MCP update tool performs a GraphQL lookup before its REST write. This account's GraphQL quota is exhausted right now (REST calls continue to succeed — the split is REST vs GraphQL, not read vs write), so the flip is structurally unavailable from here, not merely worth retrying. Enabling auto-merge is blocked by the same two facts: it is a GraphQL mutation, and it refuses a draft PR regardless.

So one human action remains — mark this PR ready for review. After that the ordinary queue path applies; no other gate is outstanding. Flagging it here rather than reporting the PR as landed, because it is not.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 03:27
@os-musk
os-musk enabled auto-merge September 4, 2026 03:27
@os-musk
os-musk added this pull request to the merge queueSep 4, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Superseded: the handoff above is no longer needed

The previous comment ended by asking a human to mark this PR ready for review, because the GraphQL bucket was exhausted and markPullRequestReadyForReview is the only door to that flip. The quota recovered a few minutes later and this seat did it itself — so ignore that ask; nothing is waiting on a person here.

Sequence, so the record is not just an assertion:

  1. Check Changeset re-fired on the skip-changeset label event → run 100892547790, skipped.
  2. All 39 check runs now read completed, zero in progress. Every conclusion is success or skipped. The one failure in the list is the superseded Check Changeset run 100890560827 from 02:33, whose successor is the skipped run above — branch protection reads the latest run per check name, and the last two stragglers went green at 02:46:59 (Test Core (1/6)) and 02:50:11 (Lint & Repo Gates).
  3. Draft → ready: done.
  4. Auto-merge: armed.

One caveat stated rather than dressed up as certainty. The auto-merge receipt came back in the empty form, (method: , enabled at ). That form carries no established meaning — both the filled and the empty form have preceded successful landings here, and an earlier claim in this seat's notes that the empty form meant "already queued" was measured false and withdrawn. There is a reliable positive reading (a merge_group workflow run on a gh-readonly-queue/main/pr-15131-… branch) but no cheap negative one, so arming is not proof of landing. At the time of writing this PR had not yet appeared in the queue; five other entries were ahead of it.

That matters because of how the last one failed: #15008 sat armed for seven hours without landing, because it had gone mergeable_state: "dirty" and auto-merge does not fire on a conflicted PR and emits no event. If this PR is still open some hours from now, that is the first thing to check — absence of an event is not evidence of progress.


Generated by Claude Code

Merged via the queue into main with commit 4283b72Sep 4, 2026
39 of 40 checks passed
@os-musk
os-musk deleted the claude/issue-13866-disclosure-fix branch September 4, 2026 03:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() - #15131

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix
Sep 4, 2026
Merged

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute()#15131
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Part of #13866 — disclosure-completeness follow-up to #15051 (merged as a548550)

Why this exists

#15051 elevated ObjectRepository.execute()'s ctx.api to { ...callerContext, isSystem: true }, closing the third executeAction caller per #13866. Independent contract review returned exactly one finding on that PR, after it had already been merged by the maintainer: its "what widens" statement named only the static readonly strip, and that is true but materially incomplete. isSystem: true is a flag, not a behaviour — it is read by ObjectQL's registered security middleware as a total bypass, not a readonly-only one.

No implementation, census, ablation, gate, or changeset-level change. The elevation, the pins, the 5-case ablation and the patch level all stood on review — this PR only corrects the disclosure text in the three places that repeated the incomplete framing:

  1. .changeset/repo-execute-elevated-context.md
  2. the execute() docstring in packages/objectql/src/engine.ts
  3. this comment thread + a body edit on fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture #15051 itself (a merged PR's body can still be edited for the historical record — done in the same round as this PR)

A fourth file changed for mechanical reasons only: content/docs/permissions/system-context.mdx — the docstring insertion shifted every later line in engine.ts by 26, rotting one anchor (check-system-context-census.mjs's own --fix, pure line rot, no population/classification change: still 106 elevation-read sites in 20 packages across 45 files, all anchored).

The corrected "what widens" statement

ctx.api inside a repo.execute()-dispatched action handler 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() ahead of every other gate in that middleware. Every find/insert/update/delete the handler drives through this ctx.api therefore also skips, in the same stroke as the readonly strip:

BehaviourAnchorConfirmed
Security middleware total short-circuit (the gate itself)security-plugin.ts:1614-1616
RLS read scoping (getReadFilter)security-plugin.ts:4344
Field-level securitysecurity-plugin.ts:4495
CRUD permission checks (same middleware)security-plugin.ts:1616
Export permission (canExport)security-plugin.ts:4573
ADR-0103 engine-owned/append-only write guardsystem-write-guard.ts:96,120, called at security-plugin.ts:1736
package-managed / system-row / curated-capability / audience-anchor write gatessecurity-plugin.ts:1690-1724
Static readonly strip — UPDATE, single rowengine.ts:11290
Static readonly strip — UPDATE, bulkengine.ts:11473
Static readonly/runtime-owned strip — INSERT (stripRuntimeOwnedFields, covers autonumber AND readonly together)engine.ts:10025
Referential-integrity check (assertReferencesResolve)engine.ts:5892
Tenant-audit muteengine.ts:3773
Hook-session isSystem propagation (carries the flag onward, not a gate)engine.ts:3543 (buildSession)
Audit attribution label ('system' vs 'anonymous', not a gate)engine.ts:12014

Every row re-derived independently by reading the cited line on this branch (not copied from the review) — see the terminal report for the full method.

Bounded on two sides, also re-verified independently:

  • Metadata-plane schema masking (metadata-core/object-schema-fls.ts:228) is a separate REST/GraphQL schema-serving dispatch path (GET /meta/object/:name and friends) that this ctx.api surface (find/insert/update/delete/count/aggregate/execute) never calls into — confirmed by reading the file's own header (@objectstack/metadata-core depends on spec+zod only; its two consumers are @objectstack/rest and @objectstack/runtime's schema-serving exits, not the engine's registered middleware chain). isSystemdoes bypass this elsewhere in the platform (content/docs/permissions/system-context.mdx row 7) — just not reachable from this PR's widening.
  • plugin-sharing/rule-hooks.ts's insert/update materialisation skip was already retired by the maintainer's 2026-08-31 ruling on 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 — confirmed by reading the file: the skip and its announcing INFO notice are both deleted, and the surviving isSystem check at rule-hooks.ts:292 is a different skip (delete-side revocation timing, delegated to record-share-cascade.ts, not a silencer) per the file's own comment. System and user writes materialise sharing grants identically today.
  • Zero measured blast radius today: independently re-ran the chained-accessor grep (.object(x).execute() across packages/, examples/, apps/ — zero hits outside this PR's own pin test (engine-repo-execute-elevation.test.ts), against a firing positive control (.object(x).update( → 195 hits, .insert( → 49 hits) confirming the grep methodology isn't blind. So the widening is total in kind, empty in practice, today.

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 #15051 extends an existing bypass to a third dispatch path rather than introducing a new one.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths — merge-base derivation against origin/main), re-derived after the anchor-fix commit: 73 commands on the final 3-file diff. All passed except two, both pre-existing "build the whole monorepo first" prerequisites unrelated to this diff (same shape #15051 itself reported):

  • check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every workspace package's dist/)
  • @objectstack/spec check:skill-examples → needs @objectstack/client-react built (unrelated to packages/objectql/docs)

node scripts/pm/check-governed-merges.mjs --test .changeset/repo-execute-elevated-context.md content/docs/permissions/system-context.mdx packages/objectql/src/engine.tsNOT governed (0 of 3 paths hit GOVERNED_SURFACES).

The PR's own 5 pins (packages/objectql/src/engine-repo-execute-elevation.test.ts) and the package typecheck were re-run on this branch to confirm the text-only edit disturbed neither: 5 passed (5), typecheck clean, both on final head ba771e706.

Scope note

No code behaviour changes here — only prose (a changeset paragraph, a docstring, one doc anchor). Left as draft; not enqueued, not auto-merge-armed. needs:contract-review was already cleared on #15051 when it merged — flagging in the report that this seat could not mechanically re-apply a label from here (no label-write tool available) and leaving that call to the PM/reviewing seat.


Generated by Claude Code

…ass 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
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 491493b45069b9ae845b2634d29a8e876933a643 — the merge of head ba771e706b8ae3a001a7b4b6f56db7fc747fefed into base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 491493b45069b9ae845b2634d29a8e876933a643 && git checkout 491493b45069b9ae845b2634d29a8e876933a643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e ba771e706b8ae3a001a7b4b6f56db7fc747fefed && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff ba771e706b8ae3a001a7b4b6f56db7fc747fefed
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@os-muskos-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: Check Changeset resolved, and the one remaining action is not mine to take

The red gate, read rather than guessed.Check Changeset was failure (run 100890560827). Its own job log names exactly two routes: apply skip-changeset if the PR releases nothing, otherwise run pnpm changeset and name the packages — and states that an empty-frontmatter changeset is not a third option.

Route taken: skip-changeset. This PR modifies.changeset/repo-execute-elevated-context.md; it does not add one. The release it corrects is already declared on main by #15051, and this PR adds no package bump of its own — it changes what that release's note says, not what it releases. Adding a second changeset would declare a release that does not exist.

Label write — read-modify-write with compare read-back, per protocol:

  • before: documentation, size/s, tooling
  • written: the same three plus skip-changeset
  • read back live: documentation, size/s, tooling, skip-changeset — matches the union exactly, nothing stripped by a concurrent writer.

The label event re-fired the workflow: new Check Changeset run 100892547790skipped (02:43:32Z). The earlier failure run remains visible in the check-run list as a superseded run; the latest run per check name is what branch protection reads.

One claim in this PR independently re-verified, not taken from the report. The only non-prose edit here is the shifted census anchor. Reading packages/objectql/src/engine.ts at head ba771e706 line 14523: get isSystem() { return this.executionContext.isSystem; } — the ScopedContext.isSystem getter, exactly what the table row claims. The --fix re-derivation is correct. (Note for the record: the PR body's prose says the docstring shifted later lines "by 26"; the actual shift is 27, 14496 → 14523. The anchor written to the doc is right; only the prose describing the offset is off by one.)

State now: 35 of 37 check runs green or skipped; Test Core (1/6) and Lint & Repo Gates still in progress at the time of writing. Not governed (check-governed-merges.mjs --test → 0 of 3 paths). Clause-② no — prose only: no new exported symbol, no new key on a published payload.

⛔ What this seat cannot do, stated plainly rather than left as silence. The PR is still a draft. GitHub exposes draft→ready only as the GraphQL mutation markPullRequestReadyForReview; REST PATCH on a pull request has no draft field, and the MCP update tool performs a GraphQL lookup before its REST write. This account's GraphQL quota is exhausted right now (REST calls continue to succeed — the split is REST vs GraphQL, not read vs write), so the flip is structurally unavailable from here, not merely worth retrying. Enabling auto-merge is blocked by the same two facts: it is a GraphQL mutation, and it refuses a draft PR regardless.

So one human action remains — mark this PR ready for review. After that the ordinary queue path applies; no other gate is outstanding. Flagging it here rather than reporting the PR as landed, because it is not.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 03:27
@os-musk
os-musk enabled auto-merge September 4, 2026 03:27
@os-musk
os-musk added this pull request to the merge queueSep 4, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Superseded: the handoff above is no longer needed

The previous comment ended by asking a human to mark this PR ready for review, because the GraphQL bucket was exhausted and markPullRequestReadyForReview is the only door to that flip. The quota recovered a few minutes later and this seat did it itself — so ignore that ask; nothing is waiting on a person here.

Sequence, so the record is not just an assertion:

  1. Check Changeset re-fired on the skip-changeset label event → run 100892547790, skipped.
  2. All 39 check runs now read completed, zero in progress. Every conclusion is success or skipped. The one failure in the list is the superseded Check Changeset run 100890560827 from 02:33, whose successor is the skipped run above — branch protection reads the latest run per check name, and the last two stragglers went green at 02:46:59 (Test Core (1/6)) and 02:50:11 (Lint & Repo Gates).
  3. Draft → ready: done.
  4. Auto-merge: armed.

One caveat stated rather than dressed up as certainty. The auto-merge receipt came back in the empty form, (method: , enabled at ). That form carries no established meaning — both the filled and the empty form have preceded successful landings here, and an earlier claim in this seat's notes that the empty form meant "already queued" was measured false and withdrawn. There is a reliable positive reading (a merge_group workflow run on a gh-readonly-queue/main/pr-15131-… branch) but no cheap negative one, so arming is not proof of landing. At the time of writing this PR had not yet appeared in the queue; five other entries were ahead of it.

That matters because of how the last one failed: #15008 sat armed for seven hours without landing, because it had gone mergeable_state: "dirty" and auto-merge does not fire on a conflicted PR and emits no event. If this PR is still open some hours from now, that is the first thing to check — absence of an event is not evidence of progress.


Generated by Claude Code

Merged via the queue into main with commit 4283b72Sep 4, 2026
39 of 40 checks passed
@os-musk
os-musk deleted the claude/issue-13866-disclosure-fix branch September 4, 2026 03:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() - #15131

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix
Sep 4, 2026
Merged

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute()#15131
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Part of #13866 — disclosure-completeness follow-up to #15051 (merged as a548550)

Why this exists

#15051 elevated ObjectRepository.execute()'s ctx.api to { ...callerContext, isSystem: true }, closing the third executeAction caller per #13866. Independent contract review returned exactly one finding on that PR, after it had already been merged by the maintainer: its "what widens" statement named only the static readonly strip, and that is true but materially incomplete. isSystem: true is a flag, not a behaviour — it is read by ObjectQL's registered security middleware as a total bypass, not a readonly-only one.

No implementation, census, ablation, gate, or changeset-level change. The elevation, the pins, the 5-case ablation and the patch level all stood on review — this PR only corrects the disclosure text in the three places that repeated the incomplete framing:

  1. .changeset/repo-execute-elevated-context.md
  2. the execute() docstring in packages/objectql/src/engine.ts
  3. this comment thread + a body edit on fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture #15051 itself (a merged PR's body can still be edited for the historical record — done in the same round as this PR)

A fourth file changed for mechanical reasons only: content/docs/permissions/system-context.mdx — the docstring insertion shifted every later line in engine.ts by 26, rotting one anchor (check-system-context-census.mjs's own --fix, pure line rot, no population/classification change: still 106 elevation-read sites in 20 packages across 45 files, all anchored).

The corrected "what widens" statement

ctx.api inside a repo.execute()-dispatched action handler 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() ahead of every other gate in that middleware. Every find/insert/update/delete the handler drives through this ctx.api therefore also skips, in the same stroke as the readonly strip:

BehaviourAnchorConfirmed
Security middleware total short-circuit (the gate itself)security-plugin.ts:1614-1616
RLS read scoping (getReadFilter)security-plugin.ts:4344
Field-level securitysecurity-plugin.ts:4495
CRUD permission checks (same middleware)security-plugin.ts:1616
Export permission (canExport)security-plugin.ts:4573
ADR-0103 engine-owned/append-only write guardsystem-write-guard.ts:96,120, called at security-plugin.ts:1736
package-managed / system-row / curated-capability / audience-anchor write gatessecurity-plugin.ts:1690-1724
Static readonly strip — UPDATE, single rowengine.ts:11290
Static readonly strip — UPDATE, bulkengine.ts:11473
Static readonly/runtime-owned strip — INSERT (stripRuntimeOwnedFields, covers autonumber AND readonly together)engine.ts:10025
Referential-integrity check (assertReferencesResolve)engine.ts:5892
Tenant-audit muteengine.ts:3773
Hook-session isSystem propagation (carries the flag onward, not a gate)engine.ts:3543 (buildSession)
Audit attribution label ('system' vs 'anonymous', not a gate)engine.ts:12014

Every row re-derived independently by reading the cited line on this branch (not copied from the review) — see the terminal report for the full method.

Bounded on two sides, also re-verified independently:

  • Metadata-plane schema masking (metadata-core/object-schema-fls.ts:228) is a separate REST/GraphQL schema-serving dispatch path (GET /meta/object/:name and friends) that this ctx.api surface (find/insert/update/delete/count/aggregate/execute) never calls into — confirmed by reading the file's own header (@objectstack/metadata-core depends on spec+zod only; its two consumers are @objectstack/rest and @objectstack/runtime's schema-serving exits, not the engine's registered middleware chain). isSystemdoes bypass this elsewhere in the platform (content/docs/permissions/system-context.mdx row 7) — just not reachable from this PR's widening.
  • plugin-sharing/rule-hooks.ts's insert/update materialisation skip was already retired by the maintainer's 2026-08-31 ruling on 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 — confirmed by reading the file: the skip and its announcing INFO notice are both deleted, and the surviving isSystem check at rule-hooks.ts:292 is a different skip (delete-side revocation timing, delegated to record-share-cascade.ts, not a silencer) per the file's own comment. System and user writes materialise sharing grants identically today.
  • Zero measured blast radius today: independently re-ran the chained-accessor grep (.object(x).execute() across packages/, examples/, apps/ — zero hits outside this PR's own pin test (engine-repo-execute-elevation.test.ts), against a firing positive control (.object(x).update( → 195 hits, .insert( → 49 hits) confirming the grep methodology isn't blind. So the widening is total in kind, empty in practice, today.

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 #15051 extends an existing bypass to a third dispatch path rather than introducing a new one.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths — merge-base derivation against origin/main), re-derived after the anchor-fix commit: 73 commands on the final 3-file diff. All passed except two, both pre-existing "build the whole monorepo first" prerequisites unrelated to this diff (same shape #15051 itself reported):

  • check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every workspace package's dist/)
  • @objectstack/spec check:skill-examples → needs @objectstack/client-react built (unrelated to packages/objectql/docs)

node scripts/pm/check-governed-merges.mjs --test .changeset/repo-execute-elevated-context.md content/docs/permissions/system-context.mdx packages/objectql/src/engine.tsNOT governed (0 of 3 paths hit GOVERNED_SURFACES).

The PR's own 5 pins (packages/objectql/src/engine-repo-execute-elevation.test.ts) and the package typecheck were re-run on this branch to confirm the text-only edit disturbed neither: 5 passed (5), typecheck clean, both on final head ba771e706.

Scope note

No code behaviour changes here — only prose (a changeset paragraph, a docstring, one doc anchor). Left as draft; not enqueued, not auto-merge-armed. needs:contract-review was already cleared on #15051 when it merged — flagging in the report that this seat could not mechanically re-apply a label from here (no label-write tool available) and leaving that call to the PM/reviewing seat.


Generated by Claude Code

…ass 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
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 491493b45069b9ae845b2634d29a8e876933a643 — the merge of head ba771e706b8ae3a001a7b4b6f56db7fc747fefed into base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 491493b45069b9ae845b2634d29a8e876933a643 && git checkout 491493b45069b9ae845b2634d29a8e876933a643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e ba771e706b8ae3a001a7b4b6f56db7fc747fefed && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff ba771e706b8ae3a001a7b4b6f56db7fc747fefed
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@os-muskos-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: Check Changeset resolved, and the one remaining action is not mine to take

The red gate, read rather than guessed.Check Changeset was failure (run 100890560827). Its own job log names exactly two routes: apply skip-changeset if the PR releases nothing, otherwise run pnpm changeset and name the packages — and states that an empty-frontmatter changeset is not a third option.

Route taken: skip-changeset. This PR modifies.changeset/repo-execute-elevated-context.md; it does not add one. The release it corrects is already declared on main by #15051, and this PR adds no package bump of its own — it changes what that release's note says, not what it releases. Adding a second changeset would declare a release that does not exist.

Label write — read-modify-write with compare read-back, per protocol:

  • before: documentation, size/s, tooling
  • written: the same three plus skip-changeset
  • read back live: documentation, size/s, tooling, skip-changeset — matches the union exactly, nothing stripped by a concurrent writer.

The label event re-fired the workflow: new Check Changeset run 100892547790skipped (02:43:32Z). The earlier failure run remains visible in the check-run list as a superseded run; the latest run per check name is what branch protection reads.

One claim in this PR independently re-verified, not taken from the report. The only non-prose edit here is the shifted census anchor. Reading packages/objectql/src/engine.ts at head ba771e706 line 14523: get isSystem() { return this.executionContext.isSystem; } — the ScopedContext.isSystem getter, exactly what the table row claims. The --fix re-derivation is correct. (Note for the record: the PR body's prose says the docstring shifted later lines "by 26"; the actual shift is 27, 14496 → 14523. The anchor written to the doc is right; only the prose describing the offset is off by one.)

State now: 35 of 37 check runs green or skipped; Test Core (1/6) and Lint & Repo Gates still in progress at the time of writing. Not governed (check-governed-merges.mjs --test → 0 of 3 paths). Clause-② no — prose only: no new exported symbol, no new key on a published payload.

⛔ What this seat cannot do, stated plainly rather than left as silence. The PR is still a draft. GitHub exposes draft→ready only as the GraphQL mutation markPullRequestReadyForReview; REST PATCH on a pull request has no draft field, and the MCP update tool performs a GraphQL lookup before its REST write. This account's GraphQL quota is exhausted right now (REST calls continue to succeed — the split is REST vs GraphQL, not read vs write), so the flip is structurally unavailable from here, not merely worth retrying. Enabling auto-merge is blocked by the same two facts: it is a GraphQL mutation, and it refuses a draft PR regardless.

So one human action remains — mark this PR ready for review. After that the ordinary queue path applies; no other gate is outstanding. Flagging it here rather than reporting the PR as landed, because it is not.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 03:27
@os-musk
os-musk enabled auto-merge September 4, 2026 03:27
@os-musk
os-musk added this pull request to the merge queueSep 4, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Superseded: the handoff above is no longer needed

The previous comment ended by asking a human to mark this PR ready for review, because the GraphQL bucket was exhausted and markPullRequestReadyForReview is the only door to that flip. The quota recovered a few minutes later and this seat did it itself — so ignore that ask; nothing is waiting on a person here.

Sequence, so the record is not just an assertion:

  1. Check Changeset re-fired on the skip-changeset label event → run 100892547790, skipped.
  2. All 39 check runs now read completed, zero in progress. Every conclusion is success or skipped. The one failure in the list is the superseded Check Changeset run 100890560827 from 02:33, whose successor is the skipped run above — branch protection reads the latest run per check name, and the last two stragglers went green at 02:46:59 (Test Core (1/6)) and 02:50:11 (Lint & Repo Gates).
  3. Draft → ready: done.
  4. Auto-merge: armed.

One caveat stated rather than dressed up as certainty. The auto-merge receipt came back in the empty form, (method: , enabled at ). That form carries no established meaning — both the filled and the empty form have preceded successful landings here, and an earlier claim in this seat's notes that the empty form meant "already queued" was measured false and withdrawn. There is a reliable positive reading (a merge_group workflow run on a gh-readonly-queue/main/pr-15131-… branch) but no cheap negative one, so arming is not proof of landing. At the time of writing this PR had not yet appeared in the queue; five other entries were ahead of it.

That matters because of how the last one failed: #15008 sat armed for seven hours without landing, because it had gone mergeable_state: "dirty" and auto-merge does not fire on a conflicted PR and emits no event. If this PR is still open some hours from now, that is the first thing to check — absence of an event is not evidence of progress.


Generated by Claude Code

Merged via the queue into main with commit 4283b72Sep 4, 2026
39 of 40 checks passed
@os-musk
os-musk deleted the claude/issue-13866-disclosure-fix branch September 4, 2026 03:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() - #15131

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix
Sep 4, 2026
Merged

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute()#15131
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Part of #13866 — disclosure-completeness follow-up to #15051 (merged as a548550)

Why this exists

#15051 elevated ObjectRepository.execute()'s ctx.api to { ...callerContext, isSystem: true }, closing the third executeAction caller per #13866. Independent contract review returned exactly one finding on that PR, after it had already been merged by the maintainer: its "what widens" statement named only the static readonly strip, and that is true but materially incomplete. isSystem: true is a flag, not a behaviour — it is read by ObjectQL's registered security middleware as a total bypass, not a readonly-only one.

No implementation, census, ablation, gate, or changeset-level change. The elevation, the pins, the 5-case ablation and the patch level all stood on review — this PR only corrects the disclosure text in the three places that repeated the incomplete framing:

  1. .changeset/repo-execute-elevated-context.md
  2. the execute() docstring in packages/objectql/src/engine.ts
  3. this comment thread + a body edit on fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture #15051 itself (a merged PR's body can still be edited for the historical record — done in the same round as this PR)

A fourth file changed for mechanical reasons only: content/docs/permissions/system-context.mdx — the docstring insertion shifted every later line in engine.ts by 26, rotting one anchor (check-system-context-census.mjs's own --fix, pure line rot, no population/classification change: still 106 elevation-read sites in 20 packages across 45 files, all anchored).

The corrected "what widens" statement

ctx.api inside a repo.execute()-dispatched action handler 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() ahead of every other gate in that middleware. Every find/insert/update/delete the handler drives through this ctx.api therefore also skips, in the same stroke as the readonly strip:

BehaviourAnchorConfirmed
Security middleware total short-circuit (the gate itself)security-plugin.ts:1614-1616
RLS read scoping (getReadFilter)security-plugin.ts:4344
Field-level securitysecurity-plugin.ts:4495
CRUD permission checks (same middleware)security-plugin.ts:1616
Export permission (canExport)security-plugin.ts:4573
ADR-0103 engine-owned/append-only write guardsystem-write-guard.ts:96,120, called at security-plugin.ts:1736
package-managed / system-row / curated-capability / audience-anchor write gatessecurity-plugin.ts:1690-1724
Static readonly strip — UPDATE, single rowengine.ts:11290
Static readonly strip — UPDATE, bulkengine.ts:11473
Static readonly/runtime-owned strip — INSERT (stripRuntimeOwnedFields, covers autonumber AND readonly together)engine.ts:10025
Referential-integrity check (assertReferencesResolve)engine.ts:5892
Tenant-audit muteengine.ts:3773
Hook-session isSystem propagation (carries the flag onward, not a gate)engine.ts:3543 (buildSession)
Audit attribution label ('system' vs 'anonymous', not a gate)engine.ts:12014

Every row re-derived independently by reading the cited line on this branch (not copied from the review) — see the terminal report for the full method.

Bounded on two sides, also re-verified independently:

  • Metadata-plane schema masking (metadata-core/object-schema-fls.ts:228) is a separate REST/GraphQL schema-serving dispatch path (GET /meta/object/:name and friends) that this ctx.api surface (find/insert/update/delete/count/aggregate/execute) never calls into — confirmed by reading the file's own header (@objectstack/metadata-core depends on spec+zod only; its two consumers are @objectstack/rest and @objectstack/runtime's schema-serving exits, not the engine's registered middleware chain). isSystemdoes bypass this elsewhere in the platform (content/docs/permissions/system-context.mdx row 7) — just not reachable from this PR's widening.
  • plugin-sharing/rule-hooks.ts's insert/update materialisation skip was already retired by the maintainer's 2026-08-31 ruling on 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 — confirmed by reading the file: the skip and its announcing INFO notice are both deleted, and the surviving isSystem check at rule-hooks.ts:292 is a different skip (delete-side revocation timing, delegated to record-share-cascade.ts, not a silencer) per the file's own comment. System and user writes materialise sharing grants identically today.
  • Zero measured blast radius today: independently re-ran the chained-accessor grep (.object(x).execute() across packages/, examples/, apps/ — zero hits outside this PR's own pin test (engine-repo-execute-elevation.test.ts), against a firing positive control (.object(x).update( → 195 hits, .insert( → 49 hits) confirming the grep methodology isn't blind. So the widening is total in kind, empty in practice, today.

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 #15051 extends an existing bypass to a third dispatch path rather than introducing a new one.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths — merge-base derivation against origin/main), re-derived after the anchor-fix commit: 73 commands on the final 3-file diff. All passed except two, both pre-existing "build the whole monorepo first" prerequisites unrelated to this diff (same shape #15051 itself reported):

  • check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every workspace package's dist/)
  • @objectstack/spec check:skill-examples → needs @objectstack/client-react built (unrelated to packages/objectql/docs)

node scripts/pm/check-governed-merges.mjs --test .changeset/repo-execute-elevated-context.md content/docs/permissions/system-context.mdx packages/objectql/src/engine.tsNOT governed (0 of 3 paths hit GOVERNED_SURFACES).

The PR's own 5 pins (packages/objectql/src/engine-repo-execute-elevation.test.ts) and the package typecheck were re-run on this branch to confirm the text-only edit disturbed neither: 5 passed (5), typecheck clean, both on final head ba771e706.

Scope note

No code behaviour changes here — only prose (a changeset paragraph, a docstring, one doc anchor). Left as draft; not enqueued, not auto-merge-armed. needs:contract-review was already cleared on #15051 when it merged — flagging in the report that this seat could not mechanically re-apply a label from here (no label-write tool available) and leaving that call to the PM/reviewing seat.


Generated by Claude Code

…ass 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
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 491493b45069b9ae845b2634d29a8e876933a643 — the merge of head ba771e706b8ae3a001a7b4b6f56db7fc747fefed into base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 491493b45069b9ae845b2634d29a8e876933a643 && git checkout 491493b45069b9ae845b2634d29a8e876933a643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e ba771e706b8ae3a001a7b4b6f56db7fc747fefed && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff ba771e706b8ae3a001a7b4b6f56db7fc747fefed
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@os-muskos-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: Check Changeset resolved, and the one remaining action is not mine to take

The red gate, read rather than guessed.Check Changeset was failure (run 100890560827). Its own job log names exactly two routes: apply skip-changeset if the PR releases nothing, otherwise run pnpm changeset and name the packages — and states that an empty-frontmatter changeset is not a third option.

Route taken: skip-changeset. This PR modifies.changeset/repo-execute-elevated-context.md; it does not add one. The release it corrects is already declared on main by #15051, and this PR adds no package bump of its own — it changes what that release's note says, not what it releases. Adding a second changeset would declare a release that does not exist.

Label write — read-modify-write with compare read-back, per protocol:

  • before: documentation, size/s, tooling
  • written: the same three plus skip-changeset
  • read back live: documentation, size/s, tooling, skip-changeset — matches the union exactly, nothing stripped by a concurrent writer.

The label event re-fired the workflow: new Check Changeset run 100892547790skipped (02:43:32Z). The earlier failure run remains visible in the check-run list as a superseded run; the latest run per check name is what branch protection reads.

One claim in this PR independently re-verified, not taken from the report. The only non-prose edit here is the shifted census anchor. Reading packages/objectql/src/engine.ts at head ba771e706 line 14523: get isSystem() { return this.executionContext.isSystem; } — the ScopedContext.isSystem getter, exactly what the table row claims. The --fix re-derivation is correct. (Note for the record: the PR body's prose says the docstring shifted later lines "by 26"; the actual shift is 27, 14496 → 14523. The anchor written to the doc is right; only the prose describing the offset is off by one.)

State now: 35 of 37 check runs green or skipped; Test Core (1/6) and Lint & Repo Gates still in progress at the time of writing. Not governed (check-governed-merges.mjs --test → 0 of 3 paths). Clause-② no — prose only: no new exported symbol, no new key on a published payload.

⛔ What this seat cannot do, stated plainly rather than left as silence. The PR is still a draft. GitHub exposes draft→ready only as the GraphQL mutation markPullRequestReadyForReview; REST PATCH on a pull request has no draft field, and the MCP update tool performs a GraphQL lookup before its REST write. This account's GraphQL quota is exhausted right now (REST calls continue to succeed — the split is REST vs GraphQL, not read vs write), so the flip is structurally unavailable from here, not merely worth retrying. Enabling auto-merge is blocked by the same two facts: it is a GraphQL mutation, and it refuses a draft PR regardless.

So one human action remains — mark this PR ready for review. After that the ordinary queue path applies; no other gate is outstanding. Flagging it here rather than reporting the PR as landed, because it is not.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 03:27
@os-musk
os-musk enabled auto-merge September 4, 2026 03:27
@os-musk
os-musk added this pull request to the merge queueSep 4, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Superseded: the handoff above is no longer needed

The previous comment ended by asking a human to mark this PR ready for review, because the GraphQL bucket was exhausted and markPullRequestReadyForReview is the only door to that flip. The quota recovered a few minutes later and this seat did it itself — so ignore that ask; nothing is waiting on a person here.

Sequence, so the record is not just an assertion:

  1. Check Changeset re-fired on the skip-changeset label event → run 100892547790, skipped.
  2. All 39 check runs now read completed, zero in progress. Every conclusion is success or skipped. The one failure in the list is the superseded Check Changeset run 100890560827 from 02:33, whose successor is the skipped run above — branch protection reads the latest run per check name, and the last two stragglers went green at 02:46:59 (Test Core (1/6)) and 02:50:11 (Lint & Repo Gates).
  3. Draft → ready: done.
  4. Auto-merge: armed.

One caveat stated rather than dressed up as certainty. The auto-merge receipt came back in the empty form, (method: , enabled at ). That form carries no established meaning — both the filled and the empty form have preceded successful landings here, and an earlier claim in this seat's notes that the empty form meant "already queued" was measured false and withdrawn. There is a reliable positive reading (a merge_group workflow run on a gh-readonly-queue/main/pr-15131-… branch) but no cheap negative one, so arming is not proof of landing. At the time of writing this PR had not yet appeared in the queue; five other entries were ahead of it.

That matters because of how the last one failed: #15008 sat armed for seven hours without landing, because it had gone mergeable_state: "dirty" and auto-merge does not fire on a conflicted PR and emits no event. If this PR is still open some hours from now, that is the first thing to check — absence of an event is not evidence of progress.


Generated by Claude Code

Merged via the queue into main with commit 4283b72Sep 4, 2026
39 of 40 checks passed
@os-musk
os-musk deleted the claude/issue-13866-disclosure-fix branch September 4, 2026 03:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() - #15131

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix
Sep 4, 2026
Merged

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute()#15131
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Part of #13866 — disclosure-completeness follow-up to #15051 (merged as a548550)

Why this exists

#15051 elevated ObjectRepository.execute()'s ctx.api to { ...callerContext, isSystem: true }, closing the third executeAction caller per #13866. Independent contract review returned exactly one finding on that PR, after it had already been merged by the maintainer: its "what widens" statement named only the static readonly strip, and that is true but materially incomplete. isSystem: true is a flag, not a behaviour — it is read by ObjectQL's registered security middleware as a total bypass, not a readonly-only one.

No implementation, census, ablation, gate, or changeset-level change. The elevation, the pins, the 5-case ablation and the patch level all stood on review — this PR only corrects the disclosure text in the three places that repeated the incomplete framing:

  1. .changeset/repo-execute-elevated-context.md
  2. the execute() docstring in packages/objectql/src/engine.ts
  3. this comment thread + a body edit on fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture #15051 itself (a merged PR's body can still be edited for the historical record — done in the same round as this PR)

A fourth file changed for mechanical reasons only: content/docs/permissions/system-context.mdx — the docstring insertion shifted every later line in engine.ts by 26, rotting one anchor (check-system-context-census.mjs's own --fix, pure line rot, no population/classification change: still 106 elevation-read sites in 20 packages across 45 files, all anchored).

The corrected "what widens" statement

ctx.api inside a repo.execute()-dispatched action handler 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() ahead of every other gate in that middleware. Every find/insert/update/delete the handler drives through this ctx.api therefore also skips, in the same stroke as the readonly strip:

BehaviourAnchorConfirmed
Security middleware total short-circuit (the gate itself)security-plugin.ts:1614-1616
RLS read scoping (getReadFilter)security-plugin.ts:4344
Field-level securitysecurity-plugin.ts:4495
CRUD permission checks (same middleware)security-plugin.ts:1616
Export permission (canExport)security-plugin.ts:4573
ADR-0103 engine-owned/append-only write guardsystem-write-guard.ts:96,120, called at security-plugin.ts:1736
package-managed / system-row / curated-capability / audience-anchor write gatessecurity-plugin.ts:1690-1724
Static readonly strip — UPDATE, single rowengine.ts:11290
Static readonly strip — UPDATE, bulkengine.ts:11473
Static readonly/runtime-owned strip — INSERT (stripRuntimeOwnedFields, covers autonumber AND readonly together)engine.ts:10025
Referential-integrity check (assertReferencesResolve)engine.ts:5892
Tenant-audit muteengine.ts:3773
Hook-session isSystem propagation (carries the flag onward, not a gate)engine.ts:3543 (buildSession)
Audit attribution label ('system' vs 'anonymous', not a gate)engine.ts:12014

Every row re-derived independently by reading the cited line on this branch (not copied from the review) — see the terminal report for the full method.

Bounded on two sides, also re-verified independently:

  • Metadata-plane schema masking (metadata-core/object-schema-fls.ts:228) is a separate REST/GraphQL schema-serving dispatch path (GET /meta/object/:name and friends) that this ctx.api surface (find/insert/update/delete/count/aggregate/execute) never calls into — confirmed by reading the file's own header (@objectstack/metadata-core depends on spec+zod only; its two consumers are @objectstack/rest and @objectstack/runtime's schema-serving exits, not the engine's registered middleware chain). isSystemdoes bypass this elsewhere in the platform (content/docs/permissions/system-context.mdx row 7) — just not reachable from this PR's widening.
  • plugin-sharing/rule-hooks.ts's insert/update materialisation skip was already retired by the maintainer's 2026-08-31 ruling on 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 — confirmed by reading the file: the skip and its announcing INFO notice are both deleted, and the surviving isSystem check at rule-hooks.ts:292 is a different skip (delete-side revocation timing, delegated to record-share-cascade.ts, not a silencer) per the file's own comment. System and user writes materialise sharing grants identically today.
  • Zero measured blast radius today: independently re-ran the chained-accessor grep (.object(x).execute() across packages/, examples/, apps/ — zero hits outside this PR's own pin test (engine-repo-execute-elevation.test.ts), against a firing positive control (.object(x).update( → 195 hits, .insert( → 49 hits) confirming the grep methodology isn't blind. So the widening is total in kind, empty in practice, today.

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 #15051 extends an existing bypass to a third dispatch path rather than introducing a new one.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths — merge-base derivation against origin/main), re-derived after the anchor-fix commit: 73 commands on the final 3-file diff. All passed except two, both pre-existing "build the whole monorepo first" prerequisites unrelated to this diff (same shape #15051 itself reported):

  • check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every workspace package's dist/)
  • @objectstack/spec check:skill-examples → needs @objectstack/client-react built (unrelated to packages/objectql/docs)

node scripts/pm/check-governed-merges.mjs --test .changeset/repo-execute-elevated-context.md content/docs/permissions/system-context.mdx packages/objectql/src/engine.tsNOT governed (0 of 3 paths hit GOVERNED_SURFACES).

The PR's own 5 pins (packages/objectql/src/engine-repo-execute-elevation.test.ts) and the package typecheck were re-run on this branch to confirm the text-only edit disturbed neither: 5 passed (5), typecheck clean, both on final head ba771e706.

Scope note

No code behaviour changes here — only prose (a changeset paragraph, a docstring, one doc anchor). Left as draft; not enqueued, not auto-merge-armed. needs:contract-review was already cleared on #15051 when it merged — flagging in the report that this seat could not mechanically re-apply a label from here (no label-write tool available) and leaving that call to the PM/reviewing seat.


Generated by Claude Code

…ass 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
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 491493b45069b9ae845b2634d29a8e876933a643 — the merge of head ba771e706b8ae3a001a7b4b6f56db7fc747fefed into base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 491493b45069b9ae845b2634d29a8e876933a643 && git checkout 491493b45069b9ae845b2634d29a8e876933a643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e ba771e706b8ae3a001a7b4b6f56db7fc747fefed && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff ba771e706b8ae3a001a7b4b6f56db7fc747fefed
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@os-muskos-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: Check Changeset resolved, and the one remaining action is not mine to take

The red gate, read rather than guessed.Check Changeset was failure (run 100890560827). Its own job log names exactly two routes: apply skip-changeset if the PR releases nothing, otherwise run pnpm changeset and name the packages — and states that an empty-frontmatter changeset is not a third option.

Route taken: skip-changeset. This PR modifies.changeset/repo-execute-elevated-context.md; it does not add one. The release it corrects is already declared on main by #15051, and this PR adds no package bump of its own — it changes what that release's note says, not what it releases. Adding a second changeset would declare a release that does not exist.

Label write — read-modify-write with compare read-back, per protocol:

  • before: documentation, size/s, tooling
  • written: the same three plus skip-changeset
  • read back live: documentation, size/s, tooling, skip-changeset — matches the union exactly, nothing stripped by a concurrent writer.

The label event re-fired the workflow: new Check Changeset run 100892547790skipped (02:43:32Z). The earlier failure run remains visible in the check-run list as a superseded run; the latest run per check name is what branch protection reads.

One claim in this PR independently re-verified, not taken from the report. The only non-prose edit here is the shifted census anchor. Reading packages/objectql/src/engine.ts at head ba771e706 line 14523: get isSystem() { return this.executionContext.isSystem; } — the ScopedContext.isSystem getter, exactly what the table row claims. The --fix re-derivation is correct. (Note for the record: the PR body's prose says the docstring shifted later lines "by 26"; the actual shift is 27, 14496 → 14523. The anchor written to the doc is right; only the prose describing the offset is off by one.)

State now: 35 of 37 check runs green or skipped; Test Core (1/6) and Lint & Repo Gates still in progress at the time of writing. Not governed (check-governed-merges.mjs --test → 0 of 3 paths). Clause-② no — prose only: no new exported symbol, no new key on a published payload.

⛔ What this seat cannot do, stated plainly rather than left as silence. The PR is still a draft. GitHub exposes draft→ready only as the GraphQL mutation markPullRequestReadyForReview; REST PATCH on a pull request has no draft field, and the MCP update tool performs a GraphQL lookup before its REST write. This account's GraphQL quota is exhausted right now (REST calls continue to succeed — the split is REST vs GraphQL, not read vs write), so the flip is structurally unavailable from here, not merely worth retrying. Enabling auto-merge is blocked by the same two facts: it is a GraphQL mutation, and it refuses a draft PR regardless.

So one human action remains — mark this PR ready for review. After that the ordinary queue path applies; no other gate is outstanding. Flagging it here rather than reporting the PR as landed, because it is not.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 03:27
@os-musk
os-musk enabled auto-merge September 4, 2026 03:27
@os-musk
os-musk added this pull request to the merge queueSep 4, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Superseded: the handoff above is no longer needed

The previous comment ended by asking a human to mark this PR ready for review, because the GraphQL bucket was exhausted and markPullRequestReadyForReview is the only door to that flip. The quota recovered a few minutes later and this seat did it itself — so ignore that ask; nothing is waiting on a person here.

Sequence, so the record is not just an assertion:

  1. Check Changeset re-fired on the skip-changeset label event → run 100892547790, skipped.
  2. All 39 check runs now read completed, zero in progress. Every conclusion is success or skipped. The one failure in the list is the superseded Check Changeset run 100890560827 from 02:33, whose successor is the skipped run above — branch protection reads the latest run per check name, and the last two stragglers went green at 02:46:59 (Test Core (1/6)) and 02:50:11 (Lint & Repo Gates).
  3. Draft → ready: done.
  4. Auto-merge: armed.

One caveat stated rather than dressed up as certainty. The auto-merge receipt came back in the empty form, (method: , enabled at ). That form carries no established meaning — both the filled and the empty form have preceded successful landings here, and an earlier claim in this seat's notes that the empty form meant "already queued" was measured false and withdrawn. There is a reliable positive reading (a merge_group workflow run on a gh-readonly-queue/main/pr-15131-… branch) but no cheap negative one, so arming is not proof of landing. At the time of writing this PR had not yet appeared in the queue; five other entries were ahead of it.

That matters because of how the last one failed: #15008 sat armed for seven hours without landing, because it had gone mergeable_state: "dirty" and auto-merge does not fire on a conflicted PR and emits no event. If this PR is still open some hours from now, that is the first thing to check — absence of an event is not evidence of progress.


Generated by Claude Code

Merged via the queue into main with commit 4283b72Sep 4, 2026
39 of 40 checks passed
@os-musk
os-musk deleted the claude/issue-13866-disclosure-fix branch September 4, 2026 03:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() - #15131

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix
Sep 4, 2026
Merged

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute()#15131
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Part of #13866 — disclosure-completeness follow-up to #15051 (merged as a548550)

Why this exists

#15051 elevated ObjectRepository.execute()'s ctx.api to { ...callerContext, isSystem: true }, closing the third executeAction caller per #13866. Independent contract review returned exactly one finding on that PR, after it had already been merged by the maintainer: its "what widens" statement named only the static readonly strip, and that is true but materially incomplete. isSystem: true is a flag, not a behaviour — it is read by ObjectQL's registered security middleware as a total bypass, not a readonly-only one.

No implementation, census, ablation, gate, or changeset-level change. The elevation, the pins, the 5-case ablation and the patch level all stood on review — this PR only corrects the disclosure text in the three places that repeated the incomplete framing:

  1. .changeset/repo-execute-elevated-context.md
  2. the execute() docstring in packages/objectql/src/engine.ts
  3. this comment thread + a body edit on fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture #15051 itself (a merged PR's body can still be edited for the historical record — done in the same round as this PR)

A fourth file changed for mechanical reasons only: content/docs/permissions/system-context.mdx — the docstring insertion shifted every later line in engine.ts by 26, rotting one anchor (check-system-context-census.mjs's own --fix, pure line rot, no population/classification change: still 106 elevation-read sites in 20 packages across 45 files, all anchored).

The corrected "what widens" statement

ctx.api inside a repo.execute()-dispatched action handler 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() ahead of every other gate in that middleware. Every find/insert/update/delete the handler drives through this ctx.api therefore also skips, in the same stroke as the readonly strip:

BehaviourAnchorConfirmed
Security middleware total short-circuit (the gate itself)security-plugin.ts:1614-1616
RLS read scoping (getReadFilter)security-plugin.ts:4344
Field-level securitysecurity-plugin.ts:4495
CRUD permission checks (same middleware)security-plugin.ts:1616
Export permission (canExport)security-plugin.ts:4573
ADR-0103 engine-owned/append-only write guardsystem-write-guard.ts:96,120, called at security-plugin.ts:1736
package-managed / system-row / curated-capability / audience-anchor write gatessecurity-plugin.ts:1690-1724
Static readonly strip — UPDATE, single rowengine.ts:11290
Static readonly strip — UPDATE, bulkengine.ts:11473
Static readonly/runtime-owned strip — INSERT (stripRuntimeOwnedFields, covers autonumber AND readonly together)engine.ts:10025
Referential-integrity check (assertReferencesResolve)engine.ts:5892
Tenant-audit muteengine.ts:3773
Hook-session isSystem propagation (carries the flag onward, not a gate)engine.ts:3543 (buildSession)
Audit attribution label ('system' vs 'anonymous', not a gate)engine.ts:12014

Every row re-derived independently by reading the cited line on this branch (not copied from the review) — see the terminal report for the full method.

Bounded on two sides, also re-verified independently:

  • Metadata-plane schema masking (metadata-core/object-schema-fls.ts:228) is a separate REST/GraphQL schema-serving dispatch path (GET /meta/object/:name and friends) that this ctx.api surface (find/insert/update/delete/count/aggregate/execute) never calls into — confirmed by reading the file's own header (@objectstack/metadata-core depends on spec+zod only; its two consumers are @objectstack/rest and @objectstack/runtime's schema-serving exits, not the engine's registered middleware chain). isSystemdoes bypass this elsewhere in the platform (content/docs/permissions/system-context.mdx row 7) — just not reachable from this PR's widening.
  • plugin-sharing/rule-hooks.ts's insert/update materialisation skip was already retired by the maintainer's 2026-08-31 ruling on 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 — confirmed by reading the file: the skip and its announcing INFO notice are both deleted, and the surviving isSystem check at rule-hooks.ts:292 is a different skip (delete-side revocation timing, delegated to record-share-cascade.ts, not a silencer) per the file's own comment. System and user writes materialise sharing grants identically today.
  • Zero measured blast radius today: independently re-ran the chained-accessor grep (.object(x).execute() across packages/, examples/, apps/ — zero hits outside this PR's own pin test (engine-repo-execute-elevation.test.ts), against a firing positive control (.object(x).update( → 195 hits, .insert( → 49 hits) confirming the grep methodology isn't blind. So the widening is total in kind, empty in practice, today.

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 #15051 extends an existing bypass to a third dispatch path rather than introducing a new one.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths — merge-base derivation against origin/main), re-derived after the anchor-fix commit: 73 commands on the final 3-file diff. All passed except two, both pre-existing "build the whole monorepo first" prerequisites unrelated to this diff (same shape #15051 itself reported):

  • check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every workspace package's dist/)
  • @objectstack/spec check:skill-examples → needs @objectstack/client-react built (unrelated to packages/objectql/docs)

node scripts/pm/check-governed-merges.mjs --test .changeset/repo-execute-elevated-context.md content/docs/permissions/system-context.mdx packages/objectql/src/engine.tsNOT governed (0 of 3 paths hit GOVERNED_SURFACES).

The PR's own 5 pins (packages/objectql/src/engine-repo-execute-elevation.test.ts) and the package typecheck were re-run on this branch to confirm the text-only edit disturbed neither: 5 passed (5), typecheck clean, both on final head ba771e706.

Scope note

No code behaviour changes here — only prose (a changeset paragraph, a docstring, one doc anchor). Left as draft; not enqueued, not auto-merge-armed. needs:contract-review was already cleared on #15051 when it merged — flagging in the report that this seat could not mechanically re-apply a label from here (no label-write tool available) and leaving that call to the PM/reviewing seat.


Generated by Claude Code

…ass 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
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 491493b45069b9ae845b2634d29a8e876933a643 — the merge of head ba771e706b8ae3a001a7b4b6f56db7fc747fefed into base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 491493b45069b9ae845b2634d29a8e876933a643 && git checkout 491493b45069b9ae845b2634d29a8e876933a643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e ba771e706b8ae3a001a7b4b6f56db7fc747fefed && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff ba771e706b8ae3a001a7b4b6f56db7fc747fefed
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@os-muskos-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: Check Changeset resolved, and the one remaining action is not mine to take

The red gate, read rather than guessed.Check Changeset was failure (run 100890560827). Its own job log names exactly two routes: apply skip-changeset if the PR releases nothing, otherwise run pnpm changeset and name the packages — and states that an empty-frontmatter changeset is not a third option.

Route taken: skip-changeset. This PR modifies.changeset/repo-execute-elevated-context.md; it does not add one. The release it corrects is already declared on main by #15051, and this PR adds no package bump of its own — it changes what that release's note says, not what it releases. Adding a second changeset would declare a release that does not exist.

Label write — read-modify-write with compare read-back, per protocol:

  • before: documentation, size/s, tooling
  • written: the same three plus skip-changeset
  • read back live: documentation, size/s, tooling, skip-changeset — matches the union exactly, nothing stripped by a concurrent writer.

The label event re-fired the workflow: new Check Changeset run 100892547790skipped (02:43:32Z). The earlier failure run remains visible in the check-run list as a superseded run; the latest run per check name is what branch protection reads.

One claim in this PR independently re-verified, not taken from the report. The only non-prose edit here is the shifted census anchor. Reading packages/objectql/src/engine.ts at head ba771e706 line 14523: get isSystem() { return this.executionContext.isSystem; } — the ScopedContext.isSystem getter, exactly what the table row claims. The --fix re-derivation is correct. (Note for the record: the PR body's prose says the docstring shifted later lines "by 26"; the actual shift is 27, 14496 → 14523. The anchor written to the doc is right; only the prose describing the offset is off by one.)

State now: 35 of 37 check runs green or skipped; Test Core (1/6) and Lint & Repo Gates still in progress at the time of writing. Not governed (check-governed-merges.mjs --test → 0 of 3 paths). Clause-② no — prose only: no new exported symbol, no new key on a published payload.

⛔ What this seat cannot do, stated plainly rather than left as silence. The PR is still a draft. GitHub exposes draft→ready only as the GraphQL mutation markPullRequestReadyForReview; REST PATCH on a pull request has no draft field, and the MCP update tool performs a GraphQL lookup before its REST write. This account's GraphQL quota is exhausted right now (REST calls continue to succeed — the split is REST vs GraphQL, not read vs write), so the flip is structurally unavailable from here, not merely worth retrying. Enabling auto-merge is blocked by the same two facts: it is a GraphQL mutation, and it refuses a draft PR regardless.

So one human action remains — mark this PR ready for review. After that the ordinary queue path applies; no other gate is outstanding. Flagging it here rather than reporting the PR as landed, because it is not.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 03:27
@os-musk
os-musk enabled auto-merge September 4, 2026 03:27
@os-musk
os-musk added this pull request to the merge queueSep 4, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Superseded: the handoff above is no longer needed

The previous comment ended by asking a human to mark this PR ready for review, because the GraphQL bucket was exhausted and markPullRequestReadyForReview is the only door to that flip. The quota recovered a few minutes later and this seat did it itself — so ignore that ask; nothing is waiting on a person here.

Sequence, so the record is not just an assertion:

  1. Check Changeset re-fired on the skip-changeset label event → run 100892547790, skipped.
  2. All 39 check runs now read completed, zero in progress. Every conclusion is success or skipped. The one failure in the list is the superseded Check Changeset run 100890560827 from 02:33, whose successor is the skipped run above — branch protection reads the latest run per check name, and the last two stragglers went green at 02:46:59 (Test Core (1/6)) and 02:50:11 (Lint & Repo Gates).
  3. Draft → ready: done.
  4. Auto-merge: armed.

One caveat stated rather than dressed up as certainty. The auto-merge receipt came back in the empty form, (method: , enabled at ). That form carries no established meaning — both the filled and the empty form have preceded successful landings here, and an earlier claim in this seat's notes that the empty form meant "already queued" was measured false and withdrawn. There is a reliable positive reading (a merge_group workflow run on a gh-readonly-queue/main/pr-15131-… branch) but no cheap negative one, so arming is not proof of landing. At the time of writing this PR had not yet appeared in the queue; five other entries were ahead of it.

That matters because of how the last one failed: #15008 sat armed for seven hours without landing, because it had gone mergeable_state: "dirty" and auto-merge does not fire on a conflicted PR and emits no event. If this PR is still open some hours from now, that is the first thing to check — absence of an event is not evidence of progress.


Generated by Claude Code

Merged via the queue into main with commit 4283b72Sep 4, 2026
39 of 40 checks passed
@os-musk
os-musk deleted the claude/issue-13866-disclosure-fix branch September 4, 2026 03:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute() - #15131

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix
Sep 4, 2026
Merged

docs(objectql): disclose the security middleware's total isSystem bypass on repo.execute()#15131
os-musk merged 2 commits into
mainfrom
claude/issue-13866-disclosure-fix

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Part of #13866 — disclosure-completeness follow-up to #15051 (merged as a548550)

Why this exists

#15051 elevated ObjectRepository.execute()'s ctx.api to { ...callerContext, isSystem: true }, closing the third executeAction caller per #13866. Independent contract review returned exactly one finding on that PR, after it had already been merged by the maintainer: its "what widens" statement named only the static readonly strip, and that is true but materially incomplete. isSystem: true is a flag, not a behaviour — it is read by ObjectQL's registered security middleware as a total bypass, not a readonly-only one.

No implementation, census, ablation, gate, or changeset-level change. The elevation, the pins, the 5-case ablation and the patch level all stood on review — this PR only corrects the disclosure text in the three places that repeated the incomplete framing:

  1. .changeset/repo-execute-elevated-context.md
  2. the execute() docstring in packages/objectql/src/engine.ts
  3. this comment thread + a body edit on fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture #15051 itself (a merged PR's body can still be edited for the historical record — done in the same round as this PR)

A fourth file changed for mechanical reasons only: content/docs/permissions/system-context.mdx — the docstring insertion shifted every later line in engine.ts by 26, rotting one anchor (check-system-context-census.mjs's own --fix, pure line rot, no population/classification change: still 106 elevation-read sites in 20 packages across 45 files, all anchored).

The corrected "what widens" statement

ctx.api inside a repo.execute()-dispatched action handler 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() ahead of every other gate in that middleware. Every find/insert/update/delete the handler drives through this ctx.api therefore also skips, in the same stroke as the readonly strip:

BehaviourAnchorConfirmed
Security middleware total short-circuit (the gate itself)security-plugin.ts:1614-1616
RLS read scoping (getReadFilter)security-plugin.ts:4344
Field-level securitysecurity-plugin.ts:4495
CRUD permission checks (same middleware)security-plugin.ts:1616
Export permission (canExport)security-plugin.ts:4573
ADR-0103 engine-owned/append-only write guardsystem-write-guard.ts:96,120, called at security-plugin.ts:1736
package-managed / system-row / curated-capability / audience-anchor write gatessecurity-plugin.ts:1690-1724
Static readonly strip — UPDATE, single rowengine.ts:11290
Static readonly strip — UPDATE, bulkengine.ts:11473
Static readonly/runtime-owned strip — INSERT (stripRuntimeOwnedFields, covers autonumber AND readonly together)engine.ts:10025
Referential-integrity check (assertReferencesResolve)engine.ts:5892
Tenant-audit muteengine.ts:3773
Hook-session isSystem propagation (carries the flag onward, not a gate)engine.ts:3543 (buildSession)
Audit attribution label ('system' vs 'anonymous', not a gate)engine.ts:12014

Every row re-derived independently by reading the cited line on this branch (not copied from the review) — see the terminal report for the full method.

Bounded on two sides, also re-verified independently:

  • Metadata-plane schema masking (metadata-core/object-schema-fls.ts:228) is a separate REST/GraphQL schema-serving dispatch path (GET /meta/object/:name and friends) that this ctx.api surface (find/insert/update/delete/count/aggregate/execute) never calls into — confirmed by reading the file's own header (@objectstack/metadata-core depends on spec+zod only; its two consumers are @objectstack/rest and @objectstack/runtime's schema-serving exits, not the engine's registered middleware chain). isSystemdoes bypass this elsewhere in the platform (content/docs/permissions/system-context.mdx row 7) — just not reachable from this PR's widening.
  • plugin-sharing/rule-hooks.ts's insert/update materialisation skip was already retired by the maintainer's 2026-08-31 ruling on 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 — confirmed by reading the file: the skip and its announcing INFO notice are both deleted, and the surviving isSystem check at rule-hooks.ts:292 is a different skip (delete-side revocation timing, delegated to record-share-cascade.ts, not a silencer) per the file's own comment. System and user writes materialise sharing grants identically today.
  • Zero measured blast radius today: independently re-ran the chained-accessor grep (.object(x).execute() across packages/, examples/, apps/ — zero hits outside this PR's own pin test (engine-repo-execute-elevation.test.ts), against a firing positive control (.object(x).update( → 195 hits, .insert( → 49 hits) confirming the grep methodology isn't blind. So the widening is total in kind, empty in practice, today.

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 #15051 extends an existing bypass to a third dispatch path rather than introducing a new one.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths — merge-base derivation against origin/main), re-derived after the anchor-fix commit: 73 commands on the final 3-file diff. All passed except two, both pre-existing "build the whole monorepo first" prerequisites unrelated to this diff (same shape #15051 itself reported):

  • check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every workspace package's dist/)
  • @objectstack/spec check:skill-examples → needs @objectstack/client-react built (unrelated to packages/objectql/docs)

node scripts/pm/check-governed-merges.mjs --test .changeset/repo-execute-elevated-context.md content/docs/permissions/system-context.mdx packages/objectql/src/engine.tsNOT governed (0 of 3 paths hit GOVERNED_SURFACES).

The PR's own 5 pins (packages/objectql/src/engine-repo-execute-elevation.test.ts) and the package typecheck were re-run on this branch to confirm the text-only edit disturbed neither: 5 passed (5), typecheck clean, both on final head ba771e706.

Scope note

No code behaviour changes here — only prose (a changeset paragraph, a docstring, one doc anchor). Left as draft; not enqueued, not auto-merge-armed. needs:contract-review was already cleared on #15051 when it merged — flagging in the report that this seat could not mechanically re-apply a label from here (no label-write tool available) and leaving that call to the PM/reviewing seat.


Generated by Claude Code

…ass 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
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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 491493b45069b9ae845b2634d29a8e876933a643 — the merge of head ba771e706b8ae3a001a7b4b6f56db7fc747fefed into base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 491493b45069b9ae845b2634d29a8e876933a643 && git checkout 491493b45069b9ae845b2634d29a8e876933a643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e ba771e706b8ae3a001a7b4b6f56db7fc747fefed && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff ba771e706b8ae3a001a7b4b6f56db7fc747fefed
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Sep 4, 2026
@os-muskos-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: Check Changeset resolved, and the one remaining action is not mine to take

The red gate, read rather than guessed.Check Changeset was failure (run 100890560827). Its own job log names exactly two routes: apply skip-changeset if the PR releases nothing, otherwise run pnpm changeset and name the packages — and states that an empty-frontmatter changeset is not a third option.

Route taken: skip-changeset. This PR modifies.changeset/repo-execute-elevated-context.md; it does not add one. The release it corrects is already declared on main by #15051, and this PR adds no package bump of its own — it changes what that release's note says, not what it releases. Adding a second changeset would declare a release that does not exist.

Label write — read-modify-write with compare read-back, per protocol:

  • before: documentation, size/s, tooling
  • written: the same three plus skip-changeset
  • read back live: documentation, size/s, tooling, skip-changeset — matches the union exactly, nothing stripped by a concurrent writer.

The label event re-fired the workflow: new Check Changeset run 100892547790skipped (02:43:32Z). The earlier failure run remains visible in the check-run list as a superseded run; the latest run per check name is what branch protection reads.

One claim in this PR independently re-verified, not taken from the report. The only non-prose edit here is the shifted census anchor. Reading packages/objectql/src/engine.ts at head ba771e706 line 14523: get isSystem() { return this.executionContext.isSystem; } — the ScopedContext.isSystem getter, exactly what the table row claims. The --fix re-derivation is correct. (Note for the record: the PR body's prose says the docstring shifted later lines "by 26"; the actual shift is 27, 14496 → 14523. The anchor written to the doc is right; only the prose describing the offset is off by one.)

State now: 35 of 37 check runs green or skipped; Test Core (1/6) and Lint & Repo Gates still in progress at the time of writing. Not governed (check-governed-merges.mjs --test → 0 of 3 paths). Clause-② no — prose only: no new exported symbol, no new key on a published payload.

⛔ What this seat cannot do, stated plainly rather than left as silence. The PR is still a draft. GitHub exposes draft→ready only as the GraphQL mutation markPullRequestReadyForReview; REST PATCH on a pull request has no draft field, and the MCP update tool performs a GraphQL lookup before its REST write. This account's GraphQL quota is exhausted right now (REST calls continue to succeed — the split is REST vs GraphQL, not read vs write), so the flip is structurally unavailable from here, not merely worth retrying. Enabling auto-merge is blocked by the same two facts: it is a GraphQL mutation, and it refuses a draft PR regardless.

So one human action remains — mark this PR ready for review. After that the ordinary queue path applies; no other gate is outstanding. Flagging it here rather than reporting the PR as landed, because it is not.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 4, 2026 03:27
@os-musk
os-musk enabled auto-merge September 4, 2026 03:27
@os-musk
os-musk added this pull request to the merge queueSep 4, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Superseded: the handoff above is no longer needed

The previous comment ended by asking a human to mark this PR ready for review, because the GraphQL bucket was exhausted and markPullRequestReadyForReview is the only door to that flip. The quota recovered a few minutes later and this seat did it itself — so ignore that ask; nothing is waiting on a person here.

Sequence, so the record is not just an assertion:

  1. Check Changeset re-fired on the skip-changeset label event → run 100892547790, skipped.
  2. All 39 check runs now read completed, zero in progress. Every conclusion is success or skipped. The one failure in the list is the superseded Check Changeset run 100890560827 from 02:33, whose successor is the skipped run above — branch protection reads the latest run per check name, and the last two stragglers went green at 02:46:59 (Test Core (1/6)) and 02:50:11 (Lint & Repo Gates).
  3. Draft → ready: done.
  4. Auto-merge: armed.

One caveat stated rather than dressed up as certainty. The auto-merge receipt came back in the empty form, (method: , enabled at ). That form carries no established meaning — both the filled and the empty form have preceded successful landings here, and an earlier claim in this seat's notes that the empty form meant "already queued" was measured false and withdrawn. There is a reliable positive reading (a merge_group workflow run on a gh-readonly-queue/main/pr-15131-… branch) but no cheap negative one, so arming is not proof of landing. At the time of writing this PR had not yet appeared in the queue; five other entries were ahead of it.

That matters because of how the last one failed: #15008 sat armed for seven hours without landing, because it had gone mergeable_state: "dirty" and auto-merge does not fire on a conflicted PR and emits no event. If this PR is still open some hours from now, that is the first thing to check — absence of an event is not evidence of progress.


Generated by Claude Code

Merged via the queue into main with commit 4283b72Sep 4, 2026
39 of 40 checks passed
@os-musk
os-musk deleted the claude/issue-13866-disclosure-fix branch September 4, 2026 03:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude