Cap Glean Bulk Search Export
Clamps the bulk-export parameters on Glean search calls before they reach the Glean MCP server, so a single agent request cannot pull an entire indexed…
- Direction
- ingress
- Rego package
glean.ingress.cap_search_export- App
- glean
- Bundles
- soc2hipaagdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:bf5ad7f22ab9e693010ae9b6eccb712de9e68d662cb33db57a29d647ba086c7f
gleancap-bulk-exportdata-minimisationingresssoc2hipaagdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: allow (transform-only — never denies)
Package: glean.ingress.cap_search_export
What it does
Clamps the bulk-export parameters on Glean search calls before they reach
the Glean MCP server, so a single agent request cannot pull an entire indexed
datasource in one sweep. Two rewrites are applied to search arguments:
num_resultsceiling — when the requestednum_resultsexceeds the configured ceiling (50 in this policy; Glean permits up to 500), it is lowered to the ceiling. The request is rewritten, not denied, so the search still runs — just at a bounded page size. This enforces minimum-necessary retrieval while keeping the search useful.exhaustivestrip — whenexhaustiveis set totrueit is rewritten tofalse, neutralising Glean's full-scan sweep.exhaustive:truecombined with a highnum_resultsis the bulk-export path the Glean landscape note flags across every indexed system (Drive, Confluence, Slack, Jira, Gmail, GitHub, Salesforce, Gong, HR…), because one Glean call fans out across everything the caller can see.
Every field is read with object.get(input.payload.args, ...), so a
missing num_results or exhaustive is treated as unset and left
alone — the policy never injects a value, it only lowers an over-broad one.
A search call that requests num_results at or below the ceiling and does
not set exhaustive:true passes through completely untouched.
The policy inspects only search. It does not constrain chat (which
takes free text with no filterable retrieval bound — egress inspection is the
only lever there) or any other Glean tool.
Compliance alignment
- SOC 2 CC6.7 — supports the restriction on transmission/movement/removal
of information by bounding how much indexed corporate data any single agent
searchcan move out of Glean in one call. - HIPAA §164.502(b) / §164.514(d) — supports the minimum-necessary standard: agents retrieve result sets sized to the task rather than the 500-record maximum the API permits, and cannot trigger an exhaustive scan of PHI-bearing sources.
- GDPR Art. 5(1)(c) — data minimisation on the agent channel: the query is minimised before it reaches Glean; Art. 5(1)(d) — a bounded result set reduces the accuracy/blast-radius surface of downstream processing.
- CCPA 11 CCR §7002 — supports proportionality: retrieval of personal information stays proportionate to the disclosed purpose rather than defaulting to an exhaustive bulk sweep.
Why ingress
The over-broad request itself is the problem. Once Glean has fanned out and
returned 500 records (or an exhaustive scan), an egress policy can only mask
fields — the volume has already been retrieved, logged, and counted against
the caller's access. Rewriting num_results and exhaustive at ingress
enforces minimisation before the query executes, which is the only place the
result count can be controlled.
Tool name matching
The DTwo gateway prefixes tool names with the configured MCP server name
(e.g. glean-search, glean-mcp-search), so matching is by case-insensitive
suffix to stay portable. The Glean remote server's search tool is the bare
name search, so this policy matches:
search(unprefixed), or- any name ending in
-search(the gateway's<server>-<tool>form)
This deliberately does not match the other Glean read tools whose names
end in _search — employee_search, code_search, gmail_search,
outlook_search, and the deprecated local server's company_search /
people_profile_search. Those are governed by other policies (datasource
fencing, mailbox restriction, transcript gating). Verify the exact name your
gateway sends with the dump-input debug technique before relying on this in
production.
Argument shape
search(query, app, type, owner, from, channel, updated, after, before, sort_by_recency, exhaustive, num_results, dynamic_search_result_filters, cursor) — structured params (verified from Glean's admin docs and tool
guides). This policy reads only num_results (numeric, up to 500) and
exhaustive (boolean). All other arguments are preserved unchanged by the
rewrite via object.union.
Examples
Passed through unchanged
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "glean-search", "type": "tool" },
"payload": {
"name": "glean-search",
"args": { "query": "q3 roadmap", "app": "confluence", "num_results": 25 }
}
}
}
allow = true, no transform — the request is already within the ceiling and
does not set exhaustive.
Transformed
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "glean-search", "type": "tool" },
"payload": {
"name": "glean-search",
"args": { "query": "", "app": "gdrive", "num_results": 500, "exhaustive": true }
}
}
}
allow = true, transform rewrites the args to
{ "query": "", "app": "gdrive", "num_results": 50, "exhaustive": false } —
the ceiling is applied and the exhaustive sweep is disabled, while query and
app are preserved.
Composition
This policy bounds retrieval volume; it does not fence which datasources a search may target, nor mask record content. Pair it with:
apps/glean/fence-datasource-scope(ingress) — restricts theappdatasource enum by IdP group so agents cannot searchgong,salescloud, or HR sources they should not reach.- An egress PII/PAN redaction policy on
search/read_document/chatresponses so identifiers in whatever records are returned are masked. - A transcript-gating policy (PF-21) for
meeting_lookup, whose ownexhaustive/extract_transcriptflags are out of scope here.
Known limitations
- Ceiling is a per-tenant tuning knob. 50 is a conservative
data-minimisation default; Glean permits up to 500. Change
max_num_resultsinpolicy.mdto match your posture at import time. - Per-request caps do not stop patient pagination. An agent that walks the
cursorpage by page can still enumerate a large dataset — it just takes more calls atnum_results: 50. Detecting cursor-driven crawls requires cross-request state the policy engine does not have; use gateway audit logs / alerting to spot high-frequency paging. - Upper bound only. The policy lowers an over-large
num_results; it does not rewrite non-positive or otherwise malformed values (e.g.num_results: -1or0). Glean capsnum_resultsat 500 server-side, so the worst case without this policy is bounded at 500 records — and withexhaustiveforced tofalse, the exhaustive full-scan path (the primary bulk lever) is closed regardless. If your Glean deployment treats a non-positivenum_resultsas "unbounded", add a lower-bound branch tooverride_entries. - Only the
searchtool is covered.employee_search,code_search,gmail_search,outlook_search, andmeeting_lookupalso return large result sets (andmeeting_lookuphas its ownexhaustiveflag), but they are intentionally out of scope — govern them with datasource-fencing, mailbox-restriction, and transcript-gating policies. The deprecated local server'scompany_searchis likewise not matched; write against the remotesearchname and add a legacy alias only if a tenant still runs the archived@gleanwork/local-mcp-serverpackage. - Suffix matching assumes the gateway
<server>-<tool>hyphen convention. A search tool that surfaces with a non-hyphen separator (e.g.glean_searchending in_search) is not matched. Confirm the exact tool name fromtools/listand extendis_search_toolif needed. - No identity-based exemptions. All callers are clamped equally. If a
data-ops group legitimately needs full-page or exhaustive reads, add an
input.subject.claims-gated bypass as a separate rule. - Coercion defence is best-effort. Glean's
num_resultsis a number andexhaustivea boolean. As a bypass defence the policy neutralises the likely truthy coercions a lenient server might accept:exhaustiveis disabled when it is booleantrue, a truthy string ("true"/"1"/"yes"/"on", case-insensitive and whitespace-trimmed, so" true "and"TRUE "are caught too), or any nonzero number — closing theexhaustive:1/exhaustive:"1"gap.false,0,"false", absent, whitespace-only, and unrecognised strings are left alone.exhaustive(unlikenum_results) has no server-side cap, so a padded truthy string is trimmed and neutralised rather than left to a safety net — a single character short of the truthy set (e.g."t","enabled") is still passed through, so keep this policy paired with the egress redaction companion.num_resultsis lowered when it is a number above the ceiling or a numeric string thatto_numberparses above it (e.g."500","5e2"). It is not lowered for a padded/whitespace string (" 500 "), a hex/radix string ("0x1F4"), or any other stringto_numberrejects — those fail safe (left unchanged) and rely on Glean's server-side cap of- Because
num_resultsis hard-capped at 500 server-side, the worst case for an unhandled string is a bounded 500-record page, not an unbounded pull; the higher-risk full-scan lever (exhaustive) is closed above regardless.
- Because
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 glean.ingress.cap_search_export
# Transform-only policy — never denies, only clamps bulk-export parameters on
# Glean `search` calls.
default allow := true
# Maximum results a single `search` call may request. Glean permits up to 500;
# this conservative data-minimisation default lowers anything above it. Tune to
# your posture at import time.
max_num_results := 50
# --- Tool matching -----------------------------------------------------------
# The gateway prefixes tool names with the configured MCP server name, so we
# match case-insensitively by suffix to stay portable. Match ONLY the Glean
# `search` tool: the bare name, or the `<server>-search` form. This excludes the
# sibling read tools whose names end in `_search` (employee_search, code_search,
# gmail_search, outlook_search, company_search) — they are out of scope.
is_search_tool if {
lower(input.resource.name) == "search"
}
is_search_tool if {
endswith(lower(input.resource.name), "-search")
}
# --- Argument access (object.get everywhere — fields may be missing) ---------
args := object.get(input.payload, "args", {})
# --- Overrides ---------------------------------------------------------------
# A partial object collecting the argument rewrites that should be applied.
# Each branch is only defined when its field is present and over-broad, so a
# missing num_results / exhaustive contributes nothing (left alone).
# Lower a numeric num_results above the ceiling to the ceiling.
override_entries["num_results"] := max_num_results if {
n := object.get(args, "num_results", null)
is_number(n)
n > max_num_results
}
# Defensive: a numeric *string* num_results above the ceiling (Glean's field is
# numeric, but a lenient server might coerce "500"). to_number on a
# non-numeric string errors and the branch fails safe (leaves the value alone).
override_entries["num_results"] := max_num_results if {
n := object.get(args, "num_results", null)
is_string(n)
to_number(n) > max_num_results
}
# Disable an exhaustive sweep: force exhaustive to false when it is set true.
override_entries["exhaustive"] := false if {
exhaustive_is_true
}
exhaustive_is_true if {
object.get(args, "exhaustive", false) == true
}
# Defensive: some servers coerce truthy strings ("true"/"1"/"yes"/"on").
# `exhaustive` is boolean-semantic, so any truthy encoding should be neutralised
# — matching only "true" would leave the equally-common "1"/"yes" coercions open.
# trim_space first, or a lenient server that trims before coercing would let a
# padded " true " / "TRUE " slip past the set membership check (the bare "true"
# is caught, so the padded form is a trivial evasion of this same defence).
exhaustive_is_true if {
v := object.get(args, "exhaustive", false)
is_string(v)
lower(trim_space(v)) in {"true", "1", "yes", "on"}
}
# Defensive: a lenient server may coerce a nonzero number to true. Only true is
# ever intended, so any nonzero numeric exhaustive is treated as the sweep flag
# (0 stays falsy and is left alone).
exhaustive_is_true if {
v := object.get(args, "exhaustive", false)
is_number(v)
v != 0
}
# --- Transform ---------------------------------------------------------------
# Rewrite the args only when there is at least one override to apply, and only
# on the ingress path for the search tool. object.union preserves every other
# argument unchanged.
transform := {"transformed_payload": object.union(args, override_entries)} if {
input.action == "tool_pre_invoke"
is_search_tool
count(override_entries) > 0
} 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