--- name: Freeze the Zapier Toolset (No Self-Expansion) tags: - zapier - constrain-aggregator - ingress - soc2 publishedAt: 2026-07-12 description: | # zapier / freeze-toolset **Direction:** ingress (`tool_pre_invoke`) **Default:** deny self-modifying meta-tools for non-admins, allow otherwise **Package:** `zapier.ingress.freeze_toolset` ## What it does In its default agentic mode, Zapier MCP exposes meta-tools that let the agent **widen its own blast radius mid-session**: `enable_zapier_action` and `auto_provision_mcp` add new actions to the toolset, `write_code_action` creates an arbitrary code-execution action, and `create_zapier_skill` / `update_zapier_skill` / `delete_zapier_skill` persist Markdown instructions that future sessions auto-load — a prompt-injection persistence vector that outlives the conversation. This policy denies those six self-modifying meta-tools unless the caller's IdP `groups` claim includes `automation-admins`, converting the self-expanding aggregator into a **fixed-capability connector**. `disable_zapier_action` and every read/execute meta-tool (`execute_zapier_read_action`, `execute_zapier_write_action`, `list_enabled_zapier_actions`, `discover_zapier_actions`, `list_zapier_skills`, `get_zapier_skill`, `get_configuration_url`, `send_feedback`) pass through, so the agent can still exercise — and narrow — its existing toolset, it just cannot grow it. Missing identity claims fail closed: a caller with no `groups` claim (or no claims at all) is not exempt and is denied. ## Compliance alignment - **SOC 2 CC6.6** — supports boundary protection against external threats: the gateway's security boundary around the Zapier connector stays fixed instead of being re-drawable by the agent (or by injected instructions) mid-session. - **SOC 2 CC6.8** — supports the prevention of unauthorized software: `write_code_action` creates arbitrary code-execution actions and `enable_zapier_action` / `auto_provision_mcp` install new capabilities into the agent's toolset; this policy restricts all three to an authorized admin group. - **SOC 2 CC9.2** — supports vendor/business-partner risk management: Zapier is a single vendor surface that can reach 9,000+ downstream apps, and this policy pins what that surface is allowed to become to an admin-controlled configuration. ## Tool name matching The policy matches the six frozen meta-tools by suffix: - `*enable_zapier_action` - `*auto_provision_mcp` - `*write_code_action` - `*create_zapier_skill` - `*update_zapier_skill` - `*delete_zapier_skill` The DTwo gateway prefixes tool names with the configured MCP server name (e.g. `zapier-mcp-enable_zapier_action`), and that prefix is not standardized — matching on the suffix keeps the policy portable. Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production. `disable_zapier_action` does **not** match the `enable_zapier_action` suffix (the preceding character differs), so narrowing the toolset stays available to everyone by design. ## Argument shape None. This policy decides purely on the tool name and the caller's identity claims — it never inspects `input.payload.args`, so it is immune to argument-shape drift in Zapier's meta-tools. Identity is read via `object.get(input.subject, "claims", {})` and `object.get(claims, "groups", [])`; the `groups` claim is expected to be an **array of strings** as emitted by the tenant's IdP. ## Examples ### Allowed — read/execute meta-tool, any caller ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "zapier-mcp-execute_zapier_read_action", "type": "tool" }, "subject": { "sub": "auth0|dev", "claims": { "groups": ["engineering"] } }, "payload": { "name": "zapier-mcp-execute_zapier_read_action", "args": {} } } } ``` `allow = true`, no reason. ### Denied — non-admin tries to enable a new action ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "zapier-mcp-enable_zapier_action", "type": "tool" }, "subject": { "sub": "auth0|dev", "claims": { "groups": ["engineering"] } }, "payload": { "name": "zapier-mcp-enable_zapier_action", "args": { "action": "gmail_send_email" } } } } ``` `allow = false`, `reason = "This Zapier connector's toolset is frozen (...)"`. ### Allowed — automation admin enables a new action ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "zapier-mcp-enable_zapier_action", "type": "tool" }, "subject": { "sub": "auth0|admin", "claims": { "groups": ["engineering", "automation-admins"] } }, "payload": { "name": "zapier-mcp-enable_zapier_action", "args": { "action": "gmail_send_email" } } } } ``` `allow = true`, no reason. ## Composition This policy freezes the toolset's *shape*; it does not constrain what the already-enabled toolset can *do*. Useful companions: - A read-only-posture ingress policy denying `execute_zapier_write_action` for non-approved groups — one rule fences every write across 9,000 apps. - An app-blocklist policy that inspects the action identifier inside `execute_zapier_read_action` / `execute_zapier_write_action` arguments (e.g. deny finance apps outside a finance group). - A content policy that treats the `instructions` argument as content and scans it for non-corporate recipients or PII patterns — Zapier's server-side AI fills unspecified fields from `instructions` after the gateway has already passed the call. - For Zapier's classic (manual configuration) mode, a default-deny-unknown-tools allowlist policy pinned to the per-account tool inventory. ## Known limitations - **Agentic mode only.** The six frozen meta-tools exist only in Zapier MCP's dynamic tool-discovery (agentic) mode. In classic manual-configuration mode the policy is inert but harmless — classic tool names are `_` shapes that do not end in these suffixes. - **Group name is a placeholder.** Replace `automation-admins` with your IdP's real group name at import time, and confirm your IdP actually emits a `groups` claim in the access token (many IdPs require explicit configuration to do so). Callers whose tokens carry no `groups` claim are denied — including would-be admins. - **Group comparison is exact and case-sensitive.** Membership is a whole-string `==` on each `groups` element: `Automation-Admins`, `automation-admins-plus`, or the admin name emitted under a different claim (e.g. `roles`) never match — those callers are denied (fail closed). Match the placeholder to your IdP's group string exactly, including case. - **`groups` must be an array.** If your IdP emits `groups` as a single string or a space-delimited string, the membership check never matches and all callers are denied the frozen tools (fail closed). Adjust the `is_automation_admin` rule if your IdP uses a non-array shape. - **Skill reads still pass.** `list_zapier_skills` / `get_zapier_skill` are allowed, so a previously poisoned skill written before this policy was attached can still be *loaded*. Audit existing skills once when attaching this policy; the freeze prevents new persistence, not the reading of old state. - **Tool names verified against Zapier's official MCP docs** (docs.zapier.com, mid-2026). If Zapier renames or adds self-modifying meta-tools, extend `frozen_suffixes` accordingly — a default-deny-unknown-tools companion policy catches such drift automatically. - **Name drift is the residual bypass.** Suffix matching (`endswith`) is exact on the trailing bytes of `input.resource.name`, so any *extended* variant of a frozen name does **not** match and is allowed — a hypothetical `enable_zapier_action_v2`, but equally a name carrying a trailing space or newline (`enable_zapier_action\n`). Tool names are set by the upstream MCP server, not by the caller, so this is not a caller-controlled bypass on Zapier's hosted server; but this policy cannot anticipate names that do not exist yet. Pair it with the default-deny-unknown-tools allowlist companion if you need drift to fail closed. - **Matching keys on `input.resource.name`.** This is the canonical PARC tool-name field, reliably populated on every `tool_pre_invoke` hook and carrying the same value as the legacy `payload.name` alias. The policy never reads `payload.name`, so it does not depend on the deprecated alias. > **Compliance note.** This policy supports alignment with the cited framework controls **on the MCP path only**. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it. direction: ingress apps: - zapier industries: [] bundles: - soc2 experimental: true schemaVersion: 1.0.0 minimumGatewayVersion: 1.0.0b24 --- ```rego package zapier.ingress.freeze_toolset # Deny-by-default: only the explicit allow rules below permit the request. default allow := false # IdP group exempted from the freeze. PLACEHOLDER — map to your tenant's real # IdP group name at import time. admin_group := "automation-admins" # The six self-modifying Zapier meta-tools (agentic mode). Each one lets the # agent change its own capability set: # enable_zapier_action / auto_provision_mcp — add new actions to the toolset # write_code_action — create an arbitrary code-execution action # create/update/delete_zapier_skill — persist instructions future sessions auto-load # Matched by suffix because the gateway prefixes tool names with the configured # MCP server name (e.g. `zapier-mcp-enable_zapier_action`). frozen_suffixes := [ "enable_zapier_action", "auto_provision_mcp", "write_code_action", "create_zapier_skill", "update_zapier_skill", "delete_zapier_skill", ] # The tool being called is one of the frozen self-modifying meta-tools. # Note: `disable_zapier_action` does NOT end with `enable_zapier_action` # (preceding character differs), so narrowing the toolset always passes. is_frozen_tool if { name := lower(input.resource.name) some suffix in frozen_suffixes endswith(name, suffix) } # Caller is an automation admin. Fails closed: if `subject`, `claims`, or # `groups` is missing (or `groups` is not an array), no membership is found # and the caller is not exempt. is_automation_admin if { claims := object.get(input.subject, "claims", {}) groups := object.get(claims, "groups", []) some group in groups group == admin_group } # Pass through every tool that does not modify the toolset — including # disable_zapier_action and all read/execute meta-tools. allow if { not is_frozen_tool } # Automation admins may modify the toolset. allow if { is_frozen_tool is_automation_admin } reasons contains "This Zapier connector's toolset is frozen: enabling actions, provisioning tools, code actions, and Zapier skill changes are restricted to automation admins. Ask an automation admin to provision the action out-of-band, then retry with your existing toolset. If you believe this is a false positive, contact your InfoSec team." if { is_frozen_tool not is_automation_admin } reason := joined if { count(reasons) > 0 reason_list := sort([r | some r in reasons]) joined := concat("; ", reason_list) } ```