fix(security): gate the MCP action surface on ai.exposed, fail-closed (#2849)#2964
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 22 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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
marked this pull request as ready for review
July 15, 2026 13:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the agent-facing half of #2849.
Problem
Business-action bodies (
type: script/body) execute as trusted code:buildActionEngineFacadebuilds theinsert/update/delete/findthe handler receives with noExecutionContext, 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 MCPrun_actionbridge dispatched any headless-invokable action, ignoring the spec'sai.exposedgovernance gate (ADR-0011).Consequences:
data:readagent's action writes are blocked by the ceiling") was therefore wrong.triggerDataenvelope the flow engine never reads, and without the caller's identity, so arunAs: 'user'flow fell into the user-less UNSCOPED (fail-open) path (ADR-0049 / [P0][security] Flow runAs never switches execution identity #1888).Change
Adopts the issue's recommended direction (option c) as the immediate hardening, and lays the groundwork for the longer-term
runAsconvergence:ai.exposedfail-closed gate on bothlist_actions(hidden) andrun_action(rejected) — only actions the author explicitly exposed to AI (ai: { exposed: true, description }) are enumerable/invokable via MCP. NewactionAiExposureErrorhelper, single-sourced.AutomationContext(record/object/userId/positions/permissions/tenantId/params) mirroring the record-change trigger, sorunAs: 'user'flows enforce RLS as the invoker.run_action/list_actionstool descriptions, the generatedSKILL.md,packages/mcp/README.md, andcontent/docs/ai/agents.mdx. The agent-facing action boundary is honestly stated asai.exposed+actions:execute+ the capability gate — not the data ceiling.runAs: 'user' | 'system'direction still tracked in Security: MCP action surface must gate onai.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 fromlist_actions+ rejected byrun_action(incl. an agent holding every capability); flow context now asserts a realAutomationContextand 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.🤖 Generated with Claude Code
https://claude.ai/code/session_0155bjzJZxZaZTKSMSvg6Aum
Generated by Claude Code