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

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 15, 2026 2:00pm

Request Review

@github-actions github-actions Bot 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 main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants