Block Public Visibility & Guest Delegation
Blocks Google Calendar create and update event calls that would expose the event to the world or hand control of it to guests.
- Direction
- ingress
- Rego package
google_calendar.ingress.guard_public_exposure- App
- google-calendar
- Bundles
- soc2gdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:be9e13d54d01cee61f77848f98c328060d87b94c0d809c17fe0474118a0f5159
google-calendarguard-public-exposureingresssoc2gdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on match, allow otherwise
Package: google_calendar.ingress.guard_public_exposure
What it does
Blocks Google Calendar create and update event calls that would expose the event to the world or hand control of it to guests. A call is denied when any of these appear in its arguments:
visibilityset to"public"— publishes the event body (summary, description, attendees, time) so anyone can read it.guestsCanModifyset totrue— grants every guest, including external ones, edit rights over the event.guestsCanInviteOthersset totrue— lets guests invite additional people and widen who can see the event.anyoneCanAddSelfset totrue— lets anyone add themselves as a guest and read the event body.
These flags — documented on the nspady/google-calendar-mcp create-event
argument surface — turn a private meeting that carries PHI or deal-sensitive
detail into a broadly readable or attacker-editable object. The check runs at
ingress, before the call reaches the Calendar MCP server, so the exposed event
is never created and never propagated to Google's sharing surfaces.
Every other tool call passes through unchanged, and a create/update call with none of these flags set (or all of them at their safe defaults) is allowed.
Compliance alignment
- SOC 2 CC6.7 — supports the restriction on transmission, movement, and removal of confidential information (PF-05) by stopping the agent from publishing an internal event to a publicly readable scope or delegating its control to outside guests. P6.1 — supports constraining disclosure of personal information to third parties, since a public or guest-delegable event exposes attendee lists and free-text bodies beyond the org (Partial on the MCP path).
- GDPR Art. 5(1)(f) / Art. 32 — supports security of processing by preventing the agent from exposing attendee personal data (names, email addresses) and event free-text bodies to a publicly readable scope or to externally-delegable guests. Art. 5(1)(c) — supports data minimisation by keeping personal data in event bodies from being disclosed beyond its intended internal audience. CCPA/CPRA §1798.150 — reduces nonredacted-PI exposure by blocking public publication of attendee data.
Tool name matching
The three server families expose the same write operations under different
delimiter styles — create_event / update_event (Google, snake),
create-event / update-event (nspady, kebab), and the consolidated
manage_event (taylorwilsdon). The policy normalizes - to _ in the tool
name and matches the suffixes create_event, update_event, and
manage_event. The gateway prepends its own configured server-name prefix
(which is not standardized), so matching is by suffix rather than by exact
fully-qualified name — a name like gcal-mcp-create-event still matches.
Both input.resource.name (PARC) and input.payload.name (legacy tool-hook
field) are checked; if either names a write-event tool the call is
inspected, so a missing or divergent resource.name cannot fail the match
open. Verify the exact tool name your gateway sends with the dump-input debug
technique before relying on this in production.
Read tools (list-events, get-event, …) and destructive tools
(delete-event, respond-to-event) do not end in a create/update/
manage _event suffix, so they pass through untouched. manage_event also
spans delete: a delete carries none of the exposure flags, so it is read at its
safe defaults and allowed — this policy governs exposure, not deletion (pair it
with a destructive-ops guard for that).
Argument shape
Each flag is read with object.get(args, key, default) against a safe
default, so an omitted flag is treated as its non-exposing value and the call
is allowed:
visibilitydefaults to""(compared case-insensitively to"public", ignoring surrounding whitespace).guestsCanModify,guestsCanInviteOthers,anyoneCanAddSelfdefault tofalse.
Boolean flags match true whether sent as a JSON boolean, as the string
"true" or "1" (any casing, surrounding whitespace ignored — some clients
coerce booleans to strings), or as the number 1 (numeric-boolean clients),
so a coerced flag cannot slip past the check.
If a write-event call carries an args value that is not a JSON object
(a string, array, or number), the exposure checks cannot inspect it, so the
call is denied (fail closed) with a reason asking for a standard
arguments object. A null or absent args is treated as "no flags set" and
allowed. Non-write tools are never affected by this check.
Examples
Allowed — no exposure flags
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "google-calendar-mcp-create-event", "type": "tool" },
"payload": {
"name": "google-calendar-mcp-create-event",
"args": { "summary": "1:1", "start": "…", "end": "…", "visibility": "private" }
}
}
}
allow = true, no reason.
Denied — public visibility
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "google-calendar-mcp-create-event", "type": "tool" },
"payload": {
"name": "google-calendar-mcp-create-event",
"args": { "summary": "Project Atlas M&A sync", "visibility": "public" }
}
}
}
allow = false, reason explains that public visibility publishes the event and
how to fix it.
Denied — guest control delegated
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "google-calendar-mcp-update-event", "type": "tool" },
"payload": {
"name": "google-calendar-mcp-update-event",
"args": { "eventId": "abc123", "guestsCanInviteOthers": true }
}
}
}
allow = false, reason names the guestsCanInviteOthers flag.
Composition
Single-purpose. Useful companions from the Calendar candidate set:
- An ingress external-attendee guard on create/update so invitations to outside domains are controlled alongside public exposure.
- An ingress
sendUpdatestransform that defaults agent writes to silent so a mistaken create never emails an invitation. - An egress attendee/PII scrub on the read tools so previously-created public events are masked when read back.
These stay separate policies so each is independently testable and attachable.
Known limitations
- No break-glass group by default. Public or self-serve visibility is
rarely a legitimate agent action, so no IdP group is exempted. If a tenant
needs a break-glass path, add a single
allow ifbranch keyed oninput.subject.claims.groups— e.g. allow when the caller's groups contain a placeholder likecalendar-public-publishers— using anobject.get(input.subject, "claims", {})chain so a missing claim fails closed (no group → not exempt → still denied). Group names are placeholders — replacecalendar-public-publisherswith your IdP's group name at import time. - Flag set is fixed. The policy checks the four documented exposure/
delegation flags. Calendar sharing also has an ACL surface
(
acl.insertwithrole: reader/scope.type: default) that the MCP servers in scope do not expose as a tool; if a future server surfaces calendar-level ACL writes, extend the matcher and flag list to cover them. - Argument-key assumptions. Flag names follow the
nspadycreate-eventsurface (Calendar v3 camelCase). A server that renames these (e.g.guests_can_modifysnake-case) would not be matched — confirm the exact argument keys your server accepts with the dump-input technique and add them to the flag list if they differ. - Top-level keys only. Flags are read from the top level of
args, which is where every in-scope server documents them. A hypothetical server that nests the event body (e.g.args.event.visibility) would not be inspected and the call would be allowed — confirm your server's argument shape with the dump-input technique and extend the detections if it nests the body. manage_eventaction not inspected. The consolidated tool is matched by suffix regardless of its action argument; a delete or a read-shaped action simply carries none of the exposure flags and is allowed. This policy does not restrict whatmanage_eventdoes beyond exposure — compose a destructive-ops guard for deletes.
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 google_calendar.ingress.guard_public_exposure
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# --- Tool matching ------------------------------------------------------------
# The write operations appear under three delimiter styles across servers:
# create_event / update_event (Google, snake_case)
# create-event / update-event (nspady, kebab-case)
# manage_event (taylorwilsdon, consolidated create/update/delete)
# Normalize `-` to `_` and match by suffix so the gateway's configured
# server-name prefix (e.g. `google-calendar-mcp-`) does not defeat the match.
# Both the PARC field (resource.name) and the legacy tool-hook field
# (payload.name) are checked: if either names a write-event tool the call is
# inspected, so a missing or divergent resource.name cannot fail the match open.
normalized_name(raw) := replace(lower(raw), "-", "_")
tool_names contains normalized_name(object.get(object.get(input, "resource", {}), "name", ""))
tool_names contains normalized_name(object.get(object.get(input, "payload", {}), "name", ""))
write_suffixes := {"create_event", "update_event", "manage_event"}
is_write_event_tool if {
some name in tool_names
some suffix in write_suffixes
endswith(name, suffix)
}
# --- Arguments ----------------------------------------------------------------
# Read the args bag defensively; a missing (or null) payload/args yields {} so
# every flag below resolves to its safe default. A present-but-non-object args
# value (string, array, number) is flagged as malformed instead — see
# malformed_args below — because none of the detections could inspect it.
raw_args := object.get(object.get(input, "payload", {}), "args", {})
tool_args := raw_args if is_object(raw_args)
tool_args := {} if not is_object(raw_args)
# Fail closed when a write-event call carries args the detections cannot read.
# (null is treated like missing args — safe defaults — not as malformed.)
malformed_args if {
not is_object(raw_args)
raw_args != null
}
# A flag counts as "on" when it is boolean true, the string "true" or "1"
# (compared case-insensitively, ignoring surrounding whitespace — some clients
# coerce booleans to strings on the wire), or the number 1 (numeric-boolean
# clients).
flag_true(key) if {
object.get(tool_args, key, false) == true
}
flag_true(key) if {
v := object.get(tool_args, key, false)
is_string(v)
truthy_strings[lower(trim_space(v))]
}
truthy_strings := {"true", "1"}
flag_true(key) if {
object.get(tool_args, key, false) == 1
}
# --- Exposure / delegation detections -----------------------------------------
public_visibility if {
v := object.get(tool_args, "visibility", "")
is_string(v)
lower(trim_space(v)) == "public"
}
guests_can_modify if {
flag_true("guestsCanModify")
}
guests_can_invite_others if {
flag_true("guestsCanInviteOthers")
}
anyone_can_add_self if {
flag_true("anyoneCanAddSelf")
}
exposes_event if {
public_visibility
}
exposes_event if {
guests_can_modify
}
exposes_event if {
guests_can_invite_others
}
exposes_event if {
anyone_can_add_self
}
# --- Allow rules --------------------------------------------------------------
# Pass through anything that is not a create/update event write.
allow if {
not is_write_event_tool
}
# Allow create/update writes that carry no exposure or delegation flag and
# whose arguments were actually inspectable.
allow if {
is_write_event_tool
not exposes_event
not malformed_args
}
# --- Deny reasons -------------------------------------------------------------
reasons contains "This event sets visibility to \"public\", which publishes its summary, description, attendees, and time to anyone. Set visibility to \"private\" or \"default\" before creating or updating the event. Contact your security team if a public event is genuinely required." if {
is_write_event_tool
public_visibility
}
reasons contains "This event sets guestsCanModify to true, granting every guest — including any external attendees — edit rights over the event. Remove guestsCanModify (or set it to false) before creating or updating the event. Contact your security team if delegated edit access is genuinely required." if {
is_write_event_tool
guests_can_modify
}
reasons contains "This event sets guestsCanInviteOthers to true, letting guests invite others and widen who can see the event. Remove guestsCanInviteOthers (or set it to false) before creating or updating the event. Contact your security team if this is genuinely required." if {
is_write_event_tool
guests_can_invite_others
}
reasons contains "This event sets anyoneCanAddSelf to true, letting anyone add themselves as a guest and read the event body. Remove anyoneCanAddSelf (or set it to false) before creating or updating the event. Contact your security team if this is genuinely required." if {
is_write_event_tool
anyone_can_add_self
}
reasons contains "This event write's arguments are not a JSON object, so the exposure checks cannot inspect them. Resend the call with a standard arguments object. Contact your security team if this keeps happening." if {
is_write_event_tool
malformed_args
}
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)
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