Freeze Salesforce Record Deletes
Denies all Salesforce record-deletion capability on the agent channel unless the caller's IdP groups claim contains the placeholder group sf-admins.
- Direction
- ingress
- Rego package
salesforce.ingress.freeze_record_deletes- App
- salesforce
- Bundles
- soc2crm
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:cb4a95da71019757822fbb9f01b50110685a33a680bf7621e6cbf97036379742
salesforcefreeze-destructive-opsingresscrmsoc2
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny record-delete capability, allow everything else
Package: salesforce.ingress.freeze_record_deletes
What it does
Denies all Salesforce record-deletion capability on the agent channel unless the
caller's IdP groups claim contains the placeholder group sf-admins. Salesforce
deletes are only recycle-bin recoverable for roughly 15 days, and deletes cascade to
master-detail child records — an injected or erroneous agent delete is effectively
irreversible. This policy makes a human-approved path (a Salesforce admin) the only
way records get deleted through MCP.
Three delete surfaces are covered:
- Salesforce Hosted MCP servers (
sobject-all/sobject-deletes):deleteSobjectRecord,deleteSobjectRecordByRelationship, anddeleteChildRecord— denied by tool-name suffix for callers outsidesf-admins. - Community smn2gnt/MCP-Salesforce:
delete_recordandbulk_delete_records— denied by tool-name suffix for callers outsidesf-admins. - Community tsmztech/mcp-server-salesforce:
salesforce_dml_recordsfronts every DML verb through one tool, so the policy readsarguments.operation(viaobject.get) and treats the call as safe only when the verb is one of the verified non-destructive operations (insert/update/upsert), compared case- and whitespace-insensitively. Anything else —delete, a whitespace-paddeddelete, an unrecognized verb, or a missing/empty/non-string value — fails closed and is denied, since a call whose verb cannot be confirmed non-destructive must be assumed delete-capable.
All other tools — reads, searches, creates, updates, and non-Salesforce tools — pass through unchanged.
Compliance alignment
- SOX §802 / 18 U.S.C. §1519 — anti-destruction/alteration of records: an agent cannot delete Opportunity, Order, Contract, or any other record feeding financial reporting on the MCP path; §802 / SEC Rule 2-06 — supports retention and legal-hold posture by keeping agent-driven deletion off evidence paths.
- SOC 2 PI1.5 — supports integrity of stored records by preventing agent-initiated destruction of CRM data.
- HIPAA §164.312(c) — integrity (anti-alteration/destruction) on the agent channel for health-cloud orgs whose Contacts and custom objects carry PHI; §164.530(c) — administrative safeguard limiting who can destroy records containing PHI.
- GDPR Art. 5(1)(d) — accuracy (anti-mass-corruption): stops an errant or
injected agent from bulk-erasing personal-data records (
bulk_delete_recordsincluded).
Tool name matching
Matches case-insensitively on the suffix of input.resource.name. The DTwo
gateway prefixes tool names with the configured MCP server name (e.g.
salesforce-deleteSobjectRecord for a server registered as salesforce), and that
prefix is not standardized — suffix matching keeps the policy portable across the
hosted and community dialects. Suffixes matched:
deletesobjectrecord,deletesobjectrecordbyrelationship(hostedsobject-all)deletesobjectrecord,deletechildrecord(hostedsobject-deletes)delete_record,bulk_delete_records(smn2gnt)salesforce_dml_records(tsmztech) — argument-gated, see below
Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production.
Argument shape
salesforce_dml_records(tsmztech) takesoperation(enum: insert/update/upsert/delete),objectName, andrecords[]. The policy readsoperationwithobject.get(input.payload.args, "operation", ""), trims and lowercases it, and treats the call as safe only when the result is in the allowlist{insert, update, upsert}. Everything else —delete, a whitespace-paddeddelete, a case variant, an unrecognized/future verb, or a missing, empty, or non-stringoperation— fails closed (denied for non-admins). One tool fronts all DML verbs, so any verb not confirmed non-destructive is assumed destructive. The failure direction is over-block, never under-block.- The
sf-adminsexemption readsinput.subject.claims.groupsviaobject.getchains with an empty-array default, so a missing subject, missing claims, or missinggroupsclaim deterministically fails closed (deny). - The suffix-matched delete tools are denied on name alone; their
sobject-name/id/object_typearguments are not inspected.
Examples
Allowed — read tool, untouched
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "salesforce-soql_query", "type": "tool" },
"payload": { "name": "salesforce-soql_query", "args": { "query": "SELECT Id FROM Account" } }
}
}
allow = true, no reason.
Denied — hosted delete tool without the sf-admins group
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "salesforce-deleteSobjectRecord", "type": "tool" },
"subject": { "sub": "auth0|rep@example.com", "claims": { "groups": ["sales"] } },
"payload": {
"name": "salesforce-deleteSobjectRecord",
"args": { "sobject-name": "Contact", "id": "0035g00000XyZzAAA" }
}
}
}
allow = false, reason names the tool and points to a Salesforce admin.
Denied — DML tool with the operation argument missing (fail closed)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "sf-mcp-salesforce_dml_records", "type": "tool" },
"payload": {
"name": "sf-mcp-salesforce_dml_records",
"args": { "objectName": "Contact", "records": [{ "Id": "0035g00000XyZzAAA" }] }
}
}
}
allow = false — the verb cannot be confirmed, so the call is treated as a delete.
Allowed — DML insert passes through
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "sf-mcp-salesforce_dml_records", "type": "tool" },
"payload": {
"name": "sf-mcp-salesforce_dml_records",
"args": { "operation": "insert", "objectName": "Task", "records": [{ "Subject": "Call" }] }
}
}
}
allow = true — an explicit non-delete verb is not gated by this policy.
Composition
Single-purpose: this policy only freezes record deletion. Companions:
salesforce/read-only— for orgs that block all writes on the agent channel; this policy complements it for orgs that do allow writes but want deletes human-approved.salesforce/role-gate-writes— gates create/update by IdP group and deliberately leaves deletes to this policy; attach both for full write governance.- An escape-hatch deny policy for
salesforce_execute_anonymous,apex_execute,tooling_execute, andrestful— those tools can delete records without ever matching a delete tool name (see Known limitations).
Known limitations
- Group names are placeholders — replace
sf-adminswith your IdP's group name at import time. The exemption readsinput.subject.claims.groups(array of strings) and fails closed: no IdP, no claim, or a non-arraygroupsvalue means nobody is exempt. - Escape hatches are out of scope.
salesforce_execute_anonymous(tsmztech),apex_execute,tooling_execute, andrestful(smn2gnt) can run arbitrary Apex or REST calls that delete records without matching any suffix here. Pair this policy with an escape-hatch deny — a delete freeze without it is advisory for those servers. - SOQL/SOSL cannot delete, so query tools are intentionally untouched.
- Beta-era hosted tool names are not matched. Late-2025 beta writeups showed
snake_case hosted names; the GA references use the camelCase names matched here,
and no beta-era delete tool name was verified. Validate against your deployed
server's live
tools/list. - Generic suffix collision. smn2gnt's
delete_record/bulk_delete_recordsare unprefixed snake_case and may match delete tools of other CRM MCP servers on the same gateway. The failure mode is over-blocking (those deletes also requiresf-admins), never under-blocking. - The Salesforce DX MCP server (
@salesforce/mcp) is not covered — it is developer tooling with its own 60+ tool surface; govern it separately. operationvalues other than plain strings fail closed. A numeric or objectoperationonsalesforce_dml_recordsis denied for non-admins by design; if your server coerces such values to a verb, confirm its behavior before relaxing this.
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 salesforce.ingress.freeze_record_deletes
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# Dedicated record-delete tools, matched by suffix (the gateway prefixes tool
# names with the configured MCP server name, which is not standardized).
# Verified against the Salesforce Hosted MCP GA references (sobject-all,
# sobject-deletes) and the smn2gnt community server's documented tool set.
delete_suffixes := [
# Hosted sobject-all / sobject-deletes — hard delete by record id
"deletesobjectrecord",
# Hosted sobject-all — delete via a relationship path
"deletesobjectrecordbyrelationship",
# Hosted sobject-deletes — delete a child record
"deletechildrecord",
# Community smn2gnt — single-record delete
"delete_record",
# Community smn2gnt — bulk delete by id list
"bulk_delete_records",
]
# Case-insensitive tool name; missing fields resolve to "" (never matches).
tool_name := lower(object.get(object.get(input, "resource", {}), "name", ""))
# Tool arguments; {} when payload/args are absent so lookups fail closed.
args := object.get(object.get(input, "payload", {}), "args", {})
is_delete_tool if {
some suffix in delete_suffixes
endswith(tool_name, suffix)
}
# tsmztech's salesforce_dml_records fronts insert/update/upsert/delete through
# one tool, so the verb lives in the `operation` argument, not the tool name.
is_dml_tool if {
endswith(tool_name, "salesforce_dml_records")
}
# The DML call is safe only when `operation` is one of tsmztech's verified
# non-destructive verbs (insert/update/upsert), compared case- and
# whitespace-insensitively. This is an allowlist, not a "not delete" denylist:
# a missing, empty, non-string, whitespace-padded ("delete "), or otherwise
# unrecognized verb (e.g. a future "hardDelete") leaves this undefined, so the
# call fails closed below — an unconfirmed verb is assumed delete-capable.
safe_dml_operations := {"insert", "update", "upsert"}
dml_operation_is_safe if {
op := object.get(args, "operation", "")
is_string(op)
lower(trim_space(op)) in safe_dml_operations
}
# Record-delete capability: a dedicated delete tool, or the multi-verb DML
# tool whose operation is (or must be assumed to be) delete.
is_record_delete if {
is_delete_tool
}
is_record_delete if {
is_dml_tool
not dml_operation_is_safe
}
# Placeholder IdP group — replace `sf-admins` with your IdP's group name at
# import time. Read fail-closed: missing subject/claims/groups → [] → no
# exemption. A non-array `groups` value also fails closed (no iteration).
caller_is_sf_admin if {
groups := object.get(object.get(object.get(input, "subject", {}), "claims", {}), "groups", [])
some group in groups
lower(group) == "sf-admins"
}
# Allow any tool without record-delete capability.
allow if {
not is_record_delete
}
# Allow record deletes only for sf-admins members.
allow if {
is_record_delete
caller_is_sf_admin
}
reasons contains msg if {
is_delete_tool
not caller_is_sf_admin
msg := sprintf("The tool '%s' deletes Salesforce records, which is restricted to the sf-admins group on this channel. Salesforce deletes are only recycle-bin recoverable for about 15 days and cascade to master-detail child records. Ask a Salesforce admin to perform or approve the deletion. Contact your InfoSec team if this block is a false positive.", [tool_name])
}
reasons contains msg if {
is_dml_tool
not dml_operation_is_safe
not caller_is_sf_admin
msg := sprintf("The tool '%s' fronts all Salesforce DML verbs including delete, and this call's 'operation' argument is 'delete', missing, or unreadable, so it fails closed as a delete restricted to the sf-admins group. Retry with an explicit non-delete operation (insert, update, or upsert), or ask a Salesforce admin to perform or approve the deletion. Contact your InfoSec team if this block is a false positive.", [tool_name])
}
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