dtwo Policy Store

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:

  1. It reads search_entities via object.get, accepting either an array (["meetings","salesforce"]) or a single string ("meetings").
  2. 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).
  3. If at least one Zoom-native entity survives, it transforms the call — search_entities is replaced with the filtered allowlist and all other arguments (query, page_size, …) pass through unchanged.
  4. 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_entities is read from input.payload.args with object.get, robust to a missing payload/args object (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_entities is 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 only search_entities with 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_zoom responses and blocks any external-system rows that slip through, since this ingress transform fences the request but cannot see the response.

Known limitations

  • search_entities corpus vocabulary is unverified. Zoom's landscape note confirms search_zoom takes a required search_entities argument 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. The zoom_native_entities set in policy.md is 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_meetings instead of meetings), 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_entities is 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_zoom is 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 keep query/page_size). Zoom's landscape note documents search_entities as the sole corpus selector, but the tool's argument schema is not source-verified. If a deployment's search_zoom also 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 of search_entities itself: the lookup and rewrite key are the exact lowercase string search_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 lowercase search_entities is 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 in SEARCH_ENTITIES while a token search_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 all or everything that the upstream might expand to every corpus (including external systems) is stripped by default because it is not in zoom_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 on object.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