Fence Zoom Agentic Search to Native Corpora
Constrains Zoom's agentic-search tool ( search zoom) so it can only reach Zoom-native content.
- Direction
- ingress
- Rego package
zoom.ingress.fence_agentic_search- App
- zoom
- Bundles
- soc2gdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:68cd3883f4dbe40b8b7af41c8f0a5c2a3e21efe5275b67aa76abb8382a0a0d72
zoomagentic-searchconstrain-aggregatoringresssoc2gdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke), transform-first
Default: allow (rewrite args); deny only when nothing Zoom-native remains
Package: zoom.ingress.fence_agentic_search
What it does
Constrains Zoom's agentic-search tool (*search_zoom) so it can only reach
Zoom-native content. Zoom's agentic search fans a single query out across Zoom
content and connected third-party systems — Salesforce accounts, Workday
employee/time-off records, ServiceNow tickets — with the required
search_entities argument selecting which corpora are searched. Left
unconstrained, an agent (or a prompt-injection) can laterally pull CRM,
employee/HR, and ticketing records through the Zoom connector, outside those
systems' own trust boundaries and governed connectors.
This policy rewrites search_entities at ingress, before the call reaches the
Zoom MCP server:
- It reads
search_entitiesviaobject.get, accepting either an array (["meetings","salesforce"]) or a single string ("meetings"). - It filters the requested entities down to a pinned per-tenant allowlist
of Zoom-native corpora (
zoom_native_entities), comparing case-insensitively and dropping everything else (salesforce,workday,servicenow, and any unrecognized value). - If at least one Zoom-native entity survives, it transforms the call —
search_entitiesis replaced with the filtered allowlist and all other arguments (query,page_size, …) pass through unchanged. - If no Zoom-native entity remains (the caller asked only for external or unrecognized corpora, or omitted the required argument), it denies with an actionable reason pointing the caller at the governed connector for the system they actually wanted.
The allowlist is a pinned constant (PF-28 style) documented for import, so a tenant with no Workday or ServiceNow integration still gets a clean default: external values are simply never in the set and are stripped.
This is a single, focused constraint on one tool — search_zoom — that would
otherwise reach sensitive data outside Zoom's own trust boundary. It does not
touch Zoom's transcript, recording, chat, or docs tools; compose the companion
policies below for those surfaces.
Compliance alignment
This policy instantiates policy family PF-14 (constrain-aggregator) for
Zoom's search_zoom fan-out.
- SOC 2 CC6.6 (Enforceable) — supports boundary protection against external threats by keeping the agent's search inside Zoom's trust boundary and denying lateral reach into third-party systems through the meta-connector; CC9.2 (Partial) — supports vendor/business-partner risk management by preventing uncontrolled cross-connector data pulls; CC6.8 (Partial) — supports restricting unauthorized functionality by fencing a self-expanding search surface.
- HIPAA §164.508 (Partial) — supports the authorization requirement for uses/disclosures of PHI by preventing agentic search from pulling employee/HR or other records into Zoom's fan-out along an ungoverned path that no BAA or minimum-necessary determination covers.
- GDPR Arts. 44/46 (Partial) — supports control over cross-border and cross-system transfers on agent-visible flows by keeping personal data in Salesforce/Workday/ServiceNow from being routed through the Zoom connector.
All alignment is on the MCP path only (see the compliance note below).
Tool name matching
Zoom's official workspace server uses bare snake_case verbs with no vendor
prefix (search_zoom), so only the gateway server-name prefix disambiguates.
The policy matches by suffix on lower(input.resource.name):
*search_zoom
The DTwo gateway prefixes tool names with the configured MCP server name (e.g.
zoom-workspace-search_zoom); that prefix is not standardized across
deployments, so suffix matching keeps the policy portable. Verify the exact name
your gateway sends with the dump-input debug technique before relying on this in
production.
Argument shape
search_entitiesis read frominput.payload.argswithobject.get, robust to a missingpayload/argsobject (fail closed). It is accepted as either an array of strings or a single string; any other shape (a number, an object, or an absent argument) normalizes to an empty list, which lands the call in the deny branch (fail closed).- Matching against
zoom_native_entitiesis case-insensitive — requested values are lowercased before lookup, so"Salesforce"and"SERVICENOW"are stripped just like their lowercase forms. - The rewrite preserves every other argument via
object.union(args, {...})and replaces onlysearch_entitieswith the sorted, de-duplicated allowlist match.
Examples
Transformed (external corpora stripped)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "zoom-workspace-search_zoom", "type": "tool" },
"payload": {
"name": "zoom-workspace-search_zoom",
"args": {
"query": "Q3 renewal risks",
"search_entities": ["meetings", "salesforce", "workday"],
"page_size": 20
}
}
}
}
allow = true; search_entities rewritten to ["meetings"]; query and
page_size preserved. salesforce and workday are dropped.
Allowed unchanged in effect (all-native, normalized)
A request for search_entities: "chat" is rewritten to ["chat"] — same
corpus, normalized to the allowlisted array form. allow = true.
Denied (only external corpora)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "zoom-workspace-search_zoom", "type": "tool" },
"payload": {
"name": "zoom-workspace-search_zoom",
"args": { "query": "open tickets", "search_entities": ["servicenow", "workday"] }
}
}
}
allow = false, with a reason naming the stripped corpora and pointing the
caller at the governed connector for that system. A search_zoom call that
omits search_entities entirely is denied the same way (fail closed).
Composition
This policy is single-purpose. Useful companions on the Zoom connector:
zoom/guard-transcripts-by-group(ingress) — gates transcript/summary retrieval by IdP group.zoom/redact-pii-meeting-intelligence(egress) — redacts PII in returned meeting content.- A defense-in-depth egress policy that inspects
search_zoomresponses and blocks any external-system rows that slip through, since this ingress transform fences the request but cannot see the response.
Known limitations
search_entitiescorpus vocabulary is unverified. Zoom's landscape note confirmssearch_zoomtakes a requiredsearch_entitiesargument that selects corpora and that external systems (Salesforce, Workday, ServiceNow) are among them, but the exact accepted string values — for both Zoom-native and external corpora — are not published or source-verified. Thezoom_native_entitiesset inpolicy.mdis a placeholder allowlist: replace its values with your tenant's actual Zoom-native entity vocabulary at import time. If Zoom uses different tokens (e.g.zoom_meetingsinstead ofmeetings), unedited values will strip everything and every call will deny — verify with the dump-input debug technique before relying on this in production.- Allowlist, not blocklist. Any corpus value not explicitly in
zoom_native_entitiesis stripped — including future Zoom-native corpora Zoom may add. This is deliberate (default-deny for the fan-out) but means the constant must be maintained as Zoom's native surface grows. - Idempotent rewrite. All-native requests are still rewritten (lowercased and normalized to an array). If your upstream corpus tokens are case-sensitive, adjust the allowlist and the lowercasing accordingly.
- Ingress only. This fences the request; it does not inspect the response. Pair with an egress policy if you need to catch external data that a misconfigured or renamed corpus still returns.
- Single tool. Only
*search_zoomis constrained. Other Zoom tools or community/sub-server surfaces that reach third-party data are not covered here. - Sibling arguments pass through unchanged. The rewrite replaces only
search_entities; every other argument is preserved verbatim (by design, to keepquery/page_size). Zoom's landscape note documentssearch_entitiesas the sole corpus selector, but the tool's argument schema is not source-verified. If a deployment'ssearch_zoomalso honors a second, undocumented corpus-selection argument (e.g.sources,connectors,include_external), this policy would not constrain it and external corpora could still be reached — the transform copies that sibling argument through unchanged. Confirm the full argument schema with the dump-input debug technique; if a second selector exists, extend the transform to strip or pin it too. This includes a case-variant ofsearch_entitiesitself: the lookup and rewrite key are the exact lowercase stringsearch_entities, so a sibling key that differs only in case (Search_Entities,SEARCH_ENTITIES) is treated as an unrelated argument and passes through verbatim. Standard JSON-RPC MCP tools match argument keys case-sensitively, so a lowercasesearch_entitiesis the only key the server reads and this is harmless; but if a deployment's server folds argument-key case, an attacker could smuggle external corpora past the fence inSEARCH_ENTITIESwhile a tokensearch_entities: ["meetings"]keeps the call in the transform branch. Verify your server's key-casing behavior; if it is case-insensitive, pin the key by lowercasing/normalizing all argument keys before the rewrite. - Meta / wildcard corpus values. A value such as
alloreverythingthat the upstream might expand to every corpus (including external systems) is stripped by default because it is not inzoom_native_entities— a request for only["all"]therefore denies (fail closed). Never add such an expanding token to the allowlist, or the fence is defeated at its root. - No identity-based exemptions. All callers are treated identically. To let
a designated group run cross-system search, add an
allow/passthrough branch keyed onobject.get(input.subject, "claims", {})groups (placeholder group names must be replaced with your IdP's group name at import time).
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.
Policy source (Rego)
package zoom.ingress.fence_agentic_search
# Transform-first ingress policy. On Zoom's agentic-search tool (`*search_zoom`)
# it rewrites the required `search_entities` argument to a pinned allowlist of
# Zoom-native corpora, stripping external systems (salesforce, workday,
# servicenow, ...) so the agent cannot laterally pull CRM / HR / ticketing
# records through Zoom's search fan-out. Allows by default; denies only when
# filtering leaves no Zoom-native corpus to search.
default allow := true
# --- Pinned per-tenant allowlist of Zoom-native search corpora (PF-28 style) ---
# Documented for import: replace these values with the Zoom-native entity
# vocabulary your tenant's agentic search actually exposes. External connectors
# (salesforce, workday, servicenow, ...) are intentionally ABSENT so their values
# are stripped rather than searched. Lookups are case-insensitive (values are
# lowercased before membership tests).
zoom_native_entities := {
"meetings",
"recordings",
"transcripts",
"chat",
"team_chat",
"docs",
"whiteboards",
}
# The Zoom agentic-search tool. Zoom's workspace server exposes it as a bare
# snake_case verb, so match by suffix — the gateway's server-name prefix (e.g.
# `zoom-workspace-search_zoom`) is not standardized. Verify with dump-input.
is_search_zoom if {
endswith(lower(input.resource.name), "search_zoom")
}
# Tool arguments, robust to a missing payload/args object (fail closed on absence).
args := object.get(object.get(input, "payload", {}), "args", {})
# Raw `search_entities` value exactly as sent (default [] when absent).
raw_entities := object.get(args, "search_entities", [])
# Normalize `search_entities` to an array of values, accepting an array or a
# single string. Any other shape (number, object, absent) becomes [] so the call
# fails closed into the deny branch.
requested_entities := raw_entities if is_array(raw_entities)
requested_entities := [raw_entities] if is_string(raw_entities)
requested_entities := [] if {
not is_array(raw_entities)
not is_string(raw_entities)
}
# The requested corpora that are Zoom-native, lowercased, de-duplicated, sorted.
# Non-string elements are skipped (their lower(...) call fails harmlessly).
allowed_entities := sort({e |
some raw_e in requested_entities
e := lower(raw_e)
zoom_native_entities[e]
})
# All requested corpus names, lowercased and sorted — used only for the reason.
requested_names := sort([lower(x) |
some x in requested_entities
is_string(x)
])
requested_display := concat(", ", requested_names) if count(requested_names) > 0
requested_display := "none specified" if count(requested_names) == 0
# Rewrite the call: pin `search_entities` to the Zoom-native subset, preserve
# every other argument. Fires whenever the tool is search_zoom and at least one
# Zoom-native corpus survives filtering.
transform := {
"transformed_payload": object.union(args, {"search_entities": allowed_entities}),
} if {
is_search_zoom
count(allowed_entities) > 0
}
# Deny when the search targets no Zoom-native corpus after filtering (only
# external/unrecognized values, or the required argument was missing/malformed).
allow := false if {
is_search_zoom
count(allowed_entities) == 0
}
reasons contains sprintf("Zoom agentic search is fenced to Zoom-native corpora, and this request named only external or unrecognized corpora (%s). None can be reached through Zoom's search fan-out. Query those systems through their own governed connector, or re-run search_zoom with a Zoom-native corpus. Contact your InfoSec team if a Zoom-native corpus was wrongly rejected.", [requested_display]) if {
is_search_zoom
count(allowed_entities) == 0
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Used in these guides
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa