You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(security): gate the MCP action surface on ai.exposed, fail-closed (#2964)
Gate list_actions/run_action on ai.exposed (fail-closed), forward the caller's
AutomationContext into flow-action dispatch so runAs:'user' flows enforce RLS,
audit trusted body dispatch, and correct the docs/framing to state the agent
action boundary as ai.exposed + actions:execute + capability gate — not the data
ceiling. Groundwork for the longer-term action-level runAs convergence (#2849).
Closes the agent-facing half of #2849.
Copy file name to clipboardExpand all lines: content/docs/ai/actions-as-tools.mdx
+75-41Lines changed: 75 additions & 41 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,16 +14,32 @@ reached by an LLM as a callable tool. On the **open edition** this happens
14
14
through [`@objectstack/mcp`](/docs/ai): your own AI (Claude, Cursor, any MCP
15
15
client, or a local model) connects over the Model Context Protocol, and the
16
16
server exposes two business-action tools — `list_actions` and `run_action` —
17
-
bound to the caller's principal. The agent operates the app the same way the
18
-
Console toolbar does, under the same row-level security and permissions. No
19
-
cloud service and no ObjectOS runtime are required.
17
+
bound to the caller's principal. The agent invokes actions the same way the
18
+
Console toolbar does — but only actions the author explicitly exposed to AI, and
19
+
only ones the caller is permitted to run. No cloud service and no ObjectOS
20
+
runtime are required.
21
+
22
+
<Callouttype="warn">
23
+
**Action bodies run as trusted code (#2849).** Unlike the object CRUD tools —
24
+
where every read/write is bounded by the caller's row-level security — a
25
+
`script`/`body` action's handler executes with the app's **full data
26
+
authority**: its internal `engine.insert/update/delete/find` calls carry no
27
+
caller context, so they bypass RLS/FLS (the SECURITY-DEFINER model many actions
28
+
rely on for cross-object writes like convert-lead). The boundary is therefore at
29
+
**invoke time**, not inside the body: `ai.exposed` (author opt-in) +
30
+
`requiredPermissions` (ADR-0066) decide what an agent may trigger. Expose an
31
+
action to AI only when its body is safe to run on behalf of anyone the gate lets
32
+
through. Flow actions differ — they honour the flow's `runAs` (ADR-0049) with
33
+
the caller's identity forwarded.
34
+
</Callout>
20
35
21
36
<Callouttype="info">
22
37
**ObjectOS** layers an in-product chat *runtime*
23
-
on top of these same actions: it generates one `action_<name>` tool per action,
24
-
gates them behind an `ai.exposed` opt-in, and adds a server-side approval queue.
25
-
Those pieces are called out below. The open path — the same Action reachable as
26
-
an MCP tool by your own AI — is the default.
38
+
on top of these same actions: it generates one `action_<name>` tool per action
39
+
and adds a server-side approval queue. Both the open MCP path and the ObjectOS
40
+
runtime gate on the same `ai.exposed` opt-in. Those pieces are called out below.
41
+
The open path — the same Action reachable as an MCP tool by your own AI — is the
42
+
default.
27
43
</Callout>
28
44
29
45
## The open path: Actions over MCP
@@ -34,16 +50,18 @@ bound to the caller's principal (the API key acts as the user):
34
50
35
51
| Tool | What it does |
36
52
|:---|:---|
37
-
|`list_actions`| Enumerates the invokable business actions the caller is permitted to run — name, target object, description, whether it needs a `recordId`, whether it is destructive, and its declared params. |
38
-
|`run_action`| Invokes an action by name with `{ recordId, params }`. Runs the app's registered business logic under the caller's permissions and RLS. |
53
+
|`list_actions`| Enumerates the business actions that are **AI-exposed** (`ai.exposed: true`) **and** the caller is permitted to run — name, target object, description, whether it needs a `recordId`, whether it is destructive, and its declared params. |
54
+
|`run_action`| Invokes an action by name with `{ recordId, params }`. Invocation is gated (author opt-in + capabilities); the action body then runs the app's registered logic as trusted code. |
39
55
40
56
`run_action` resolves the action and dispatches it through the framework's own
41
57
action mechanism — `IDataEngine.executeAction` for `script` / inline-`body`
42
58
actions, or the automation flow runner for `type:'flow'` — exactly the path the
43
-
REST `/actions/...` route uses. Because the bridge is bound to the caller's
44
-
`ExecutionContext`, a BYO-AI client (Claude Code, Cursor, …) can trigger real
45
-
business logic — "complete this task", "convert this lead" — under the same
46
-
guardrails as the UI.
59
+
REST `/actions/...` route uses. Invocation is bound to the caller's
60
+
`ExecutionContext` for the gate checks and subject-record load, so a BYO-AI
61
+
client (Claude Code, Cursor, …) can trigger real business logic — "complete this
62
+
task", "convert this lead". Note the body itself runs trusted (see the warning
63
+
above); the caller context bounds *whether* the action fires and what record it
0 commit comments