Skip to content

fix(security): gate the MCP action surface on ai.exposed, fail-closed (#2849) - #2964

Merged
os-zhuang merged 3 commits into
mainfrom
claude/security-unbounded-rls-fls-qusv11
Jul 15, 2026
Merged

fix(security): gate the MCP action surface on ai.exposed, fail-closed (#2849)#2964
os-zhuang merged 3 commits into
mainfrom
claude/security-unbounded-rls-fls-qusv11

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes the agent-facing half of #2849.

Problem

Business-action bodies (type: script / body) execute as trusted code: buildActionEngineFacade builds the insert/update/delete/find the handler receives with no ExecutionContext, so those calls hit the SecurityPlugin's empty-principal skip and bypass RLS / FLS / CRUD / tenant scoping entirely. That is defensible for authored app logic (convert-lead, cascade-close legitimately write across objects the caller couldn't) — but the MCP run_action bridge dispatched any headless-invokable action, ignoring the spec's ai.exposed governance gate (ADR-0011).

Consequences:

Change

Adopts the issue's recommended direction (option c) as the immediate hardening, and lays the groundwork for the longer-term runAs convergence:

  1. ai.exposed fail-closed gate on both list_actions (hidden) and run_action (rejected) — only actions the author explicitly exposed to AI (ai: { exposed: true, description }) are enumerable/invokable via MCP. New actionAiExposureError helper, single-sourced.
  2. Flow actions receive a proper AutomationContext (record / object / userId / positions / permissions / tenantId / params) mirroring the record-change trigger, so runAs: 'user' flows enforce RLS as the invoker.
  3. Audit logging of trusted body dispatch on both the MCP and REST action paths — elevation is now audible, not silent.
  4. Docs/wording corrected where they claimed action bodies run under the caller's RLS: the MCP run_action/list_actions tool descriptions, the generated SKILL.md, packages/mcp/README.md, and content/docs/ai/agents.mdx. The agent-facing action boundary is honestly stated as ai.exposed + actions:execute + the capability gate — not the data ceiling.
  5. The trusted (SECURITY-DEFINER-like) semantics of both engine facades are documented at the source, pointing at the longer-term action-level runAs: 'user' | 'system' direction still tracked in Security: MCP action surface must gate on ai.exposed — action bodies run trusted (unbounded RLS/FLS), so invoke-time is the only agent boundary (#2849) #2849.

Tests

  • packages/runtime/src/http-dispatcher.test.ts: unexposed action hidden from list_actions + rejected by run_action (incl. an agent holding every capability); flow context now asserts a real AutomationContext and caller-identity forwarding.
  • examples/app-todo/test/mcp-actions.e2e.ts: new Step 6 drives the AI-exposure gate end-to-end through the real MCP runtime.
  • runtime (509), mcp (66), objectql (852), plugin-dev (7) all green in isolation; eslint + doc-authoring guard clean; changeset added.

Scope note. This PR is the invoke-time hardening + framing correction. The larger architectural step — an action-level runAs so bodies can opt into running bounded by the caller (agents → ceiling∩user) — is deliberately left for a follow-up and remains tracked on #2849.

Unrelated pre-existing failure observed.app-todo's complete_task e2e step fails on pristine main because #2957's readonly-field enforcement rejects the handler's completed_date stamp. Independent of this change; flagging for a separate issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_0155bjzJZxZaZTKSMSvg6Aum


Generated by Claude Code

claude added 2 commits July 15, 2026 13:31
…sed (#2849)
Business-action bodies execute as trusted code: buildActionEngineFacade
carries no ExecutionContext, so a body's reads/writes hit the security
plugin's empty-principal skip and bypass RLS/FLS/CRUD/tenant scoping —
the caller's permissions and an agent's ADR-0090 D10 data ceiling do not
bound what an invoked action does internally. The MCP run_action bridge
nevertheless dispatched ANY headless-invokable action, ignoring the
spec's ai.exposed governance gate (ADR-0011).
- list_actions/run_action now fail-closed on ai.exposed: only actions
the author explicitly opted into the AI surface are enumerable and
invokable via MCP.
- Flow-type actions receive a proper AutomationContext (record/object/
userId/positions/permissions/tenantId/params) instead of the former
triggerData envelope the engine never read, so runAs:'user' flows
enforce RLS as the invoker instead of running unscoped (ADR-0049/#1888).
- Trusted body dispatch is audit-logged on both the MCP and REST action
paths, and the facades' trusted (SECURITY-DEFINER-like) semantics are
documented at the source.
- Corrected the MCP tool descriptions, README, and agents docs that
claimed action bodies run under the caller's RLS (the #2845 framing):
the agent-facing boundary is ai.exposed + actions:execute + the
capability gate, not the data ceiling.
Longer-term direction (action-level runAs:'user'|'system' mirroring
flows) stays tracked in #2849.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155bjzJZxZaZTKSMSvg6Aum
…security model
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155bjzJZxZaZTKSMSvg6Aum
@vercel

vercelBot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specCanceledCanceledJul 15, 2026 2:00pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/mcp, @objectstack/runtime.

22 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via @objectstack/mcp)
  • content/docs/ai/agents.mdx(via @objectstack/mcp)
  • content/docs/ai/index.mdx(via @objectstack/mcp)
  • content/docs/ai/natural-language-queries.mdx(via @objectstack/mcp)
  • content/docs/api/index.mdx(via @objectstack/mcp, @objectstack/runtime)
  • content/docs/api/wire-format.mdx(via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx(via packages/runtime)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/runtime)
  • content/docs/deployment/environment-variables.mdx(via @objectstack/mcp)
  • content/docs/deployment/index.mdx(via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx(via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx(via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx(via @objectstack/runtime)
  • content/docs/plugins/packages.mdx(via @objectstack/mcp, @objectstack/runtime)
  • content/docs/protocol/knowledge.mdx(via @objectstack/mcp)
  • content/docs/protocol/objectos/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/objectos/index.mdx(via @objectstack/runtime)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx(via @objectstack/mcp, @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

The open MCP path now gates on ai.exposed (not just permissions), and
action bodies run trusted rather than under the caller's RLS. Rewrite the
Actions-as-Tools doc to match: flip the 'open path does NOT use ai.exposed'
callout, distinguish RLS-bounded object CRUD from trusted action bodies,
correct the flow dispatch shape (AutomationContext, not triggerData), and
restate the permission section as invoke-time gate + trusted body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155bjzJZxZaZTKSMSvg6Aum
@os-zhuang
os-zhuang marked this pull request as ready for review July 15, 2026 13:48
@os-zhuang
os-zhuang merged commit fdfe1d9 into mainJul 15, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/security-unbounded-rls-fls-qusv11 branch July 15, 2026 14:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude