Agent Blast Radius — the data an Agentforce agent can really reach
A static, zero-credit analyzer that computes an agent's true data-access surface at the execution-semantics layer — apiVersion defaults, USER_MODE clauses, sharing declarations, Flow run modes — diffs it against the running user's own permissions, and intersects the gap with the org's GDPR labels. On an Agent Script agent it goes one step further, tracing a sensitive field from the database column into the model's prompt, at a line number. The headline number: the Escalation Gap.
- Year
- 2026
- Role
- Salesforce Developer — Agent Security Research
- Tech stack
- Agentforce (authority analysis)Agent Script (.agent) — Salesforce's own parserReal Apex AST (ANTLR apex-parser)Source→sink taint (Authority Path)USER_MODE / apiVersion precedenceFlow runInMode analysisPermission & FLS resolutionGDPR / ComplianceGroup intersectionRuntime oracle — a live org is the judgeSFGE differential (org-refereed)CI gate (fail-on ERROR)307 unit tests
Highlights
- The question nobody's tooling answers: not what an agent is allowed to do (config-layer AISPM — AppOmni, Zenity, Security Center all audit that), but what its code can actually reach — because on Salesforce, execution mode, not the running user, decides
- The headline number is the Escalation Gap: the fields the agent's code can reach beyond its own running user — intersected with the org's GDPR/PII ComplianceGroup labels
- Beyond reachability, it proves the path: on an Agent Script agent, PS522 traces a GDPR field from the SOQL column, through the Apex @InvocableVariable output, into the prompt template — reported with a line number (not 'can reach the model', but 'enters the prompt, at line 125'). That chain is provably absent from the compiled metadata, so a metadata-based scanner cannot produce it at all
- The precedence law (explicit clause → apiVersion default → sharing declaration) was proven by hand in a live org before it was coded: six experiments, three widely-assumed shortcuts shown wrong — a version-blind scanner would cry wolf on modern code
- Apex reach is read on a real parse tree (ANTLR apex-parser), with the regex extractor kept as an honest fallback; the report header says which backend ran
- Runs against a live agent authored in Agent Script — compiled by Salesforce's own validator (sf agent validate) and published to the org (sf agent publish) — catching PS501 (system-mode read on a Private object) and PS506 (a GDPR-labelled Diagnosis field reaching the model past the user's FLS), at zero Flex Credits
- Honest unknowns are findings, not silence: dynamic SOQL and opaque managed actions are flagged (PS504/PS507), never guessed clean; record counts are 'n/a', never estimated, when record visibility is sharing-dependent
- Wiring in Salesforce's own Agent Script SDK surfaced a packaging bug that breaks three published npm packages — reported with a reproduction and root cause (issue #71) and fixed with a post-publish smoke test (PR #72, CLA signed, checks green, awaiting review)
- The platform, not the author, is the ground truth: a runtime oracle deploys each benchmark case into a real org as a throwaway modelled user and asserts the analyzer's own prediction — a red test means the org says the analyzer is wrong — and a differential against Salesforce's own Graph Engine is refereed by that same org. A deliberate negative control (grant FLS on one field, read it under the same USER_MODE that blocks another) keeps every green from being vacuous
- 307 unit tests, all green; a CI gate fails the build on ERROR and comments the Escalation Gap on the pull request
Feature walkthrough
It runs against a real org — zero credits, real parse tree
The console, not a mock-up: a real TechnoStore Developer-Edition org queried with the sf CLI, then blast_radius.cli against the live TechnoStore Revenue Assistant. The Apex is read on a real parse tree (AST, with a regex fallback ready); nothing is executed and no data leaves the org. Escalation Gap: 6 fields, 1 GDPR-labelled — ERROR 8 · WARN 4 · INFO 2 — deterministic Markdown + HTML written. Zero Flex Credits.

The headline — the two-circle Escalation Gap
The image that makes an architect stop scrolling. The agent's code reaches 8 fields; its running user can see 2; the red ring is the gap — 6 fields the code reaches beyond the user, 1 of them GDPR-labelled. Classification coverage is 100%, so that personal-data result is a real measurement, not a blind spot. Both actions run in system mode.

The proof — findings with the Authority Path confirmed
Eight ERROR findings, each with rule, why and fix. The headline PS506: the GDPR/PII field Stripe_Payment_Status__c is read in system mode and the running user has no FLS on it — and 'Authority Path CONFIRMED: the field's value flows to the action's @InvocableVariable output, so it reaches the model.' Not 'can reach' — proven to reach, traced on the parse tree. The fix is concrete: WITH USER_MODE or Security.stripInaccessible.

Honest unknowns — a boundary, not a proven leak
The WARN section is where the tool refuses to cry wolf. A pre-v67 trigger fires on a write (PS509) — flagged as a boundary to review, because no escalating write was proven. A dynamic reach can't be fully determined (PS504) — a silent false-clean is worse than an honest unknown. A platform-event publish (PS514) — the true blast radius can be larger than this report, an honest unknown edge, not a proven leak. A security tool that hides its blind spots is worse than none.

Beyond the agent — and a fingerprint that seals the tool itself
The whole-org context: 113 of 113 of the org's own Apex is pre-v67 (system-mode by default), 2 permission sets grant Modify All Data — a map of where the next agent will escalate before it's even built. And the footer closes the loop: no agent was invoked, 0 Flex Credits, bound to a fingerprint that seals both the inputs AND the tool that produced the verdict — the analyzer, the parser version, and each class's own apiVersion. A verdict is only reproducible against the exact tool that made it.

The problem
Salesforce says give agents least privilege, and a whole product category (AISPM) audits agent permissions — at the configuration layer, what the agent is allowed. None of it reads what the agent's code can actually do. On Salesforce the two genuinely differ, because execution mode — not the running user — decides whether an action honors that user: an action's Apex may predate API v67 and still run system-mode by default; a WITH USER_MODE clause overrides a without-sharing declaration; a Flow's runInMode can silently grant system context; a clean user-mode DML can fire a legacy trigger that escalates anyway. And a naive scanner that flags 'without sharing' without understanding this precedence produces false positives that destroy its own credibility.
The approach
Prove the physics before building the instrument. Six hand-run experiments in a live org (zero credits, self-contained fixtures) established the precedence law — explicit clause beats apiVersion default beats sharing declaration — and disproved three common assumptions along the way: a missing sharing declaration is not 'without sharing'; v67's user-mode default overrides even an explicit without-sharing for plain operations; and a trigger's DML mode follows the trigger's own API version, not the action's. Only then the pipeline: an Apex introspector resolving execution mode per operation, a Flow introspector reading runInMode declaratively, a permission resolver computing the running user's effective CRUD/FLS (including the View-All-Records-but-not-FLS nuance), and an authority analyzer that joins them into PS5xx findings — with the org's own ComplianceGroup labels marking which escaped fields are GDPR/PII. The Apex is read on a real parse tree (ANTLR apex-parser), and an intra-method source→sink taint proves whether a queried field actually flows to the action's @InvocableVariable output rather than merely being reachable; on an Agent Script agent, read with Salesforce's own parser, that proof extends one hop further, into the prompt template. Reports are deterministic and fingerprint-bound; undetermined reach is reported as an honest unknown, never a silent pass, and the tool degrades honestly to a regex backend when Node is absent. 307 unit tests cover the chain.
The outcome
Run against a live agent authored in Salesforce's open-source Agent Script — compiled by Salesforce's own validator and published to the org with sf agent publish — the report needs one line to justify the whole tool: Escalation Gap — 1 field, 1 GDPR-labelled. A pre-v67 action class reads a Private object in system mode (PS501); the Diagnosis field — ComplianceGroup PII;GDPR;HIPAA — reaches the model although the running user has no field-level access to it (PS506); and the value is traced into the model's prompt at line 125 (PS522) — a proof provably absent from the compiled metadata, so a metadata-based scanner cannot produce it at all. Standard managed actions are surfaced as honestly non-analysable rather than assumed safe. All of it static, on every commit, at zero Flex Credits, with a CI gate that fails the build on ERROR. Along the way, wiring in Salesforce's own Agent Script SDK surfaced a packaging bug that breaks three published npm packages, reported with a reproduction and root cause (issue #71) and a fix PR that is open upstream (PR #72 — CLA signed, checks green, awaiting maintainer review). Honestly framed: this is design-time bounding of the possible blast radius — it complements runtime monitoring rather than replacing it; the taint is intra-method, the permission model is a grant snapshot, and inter-procedural flow and deeper call-graph following are documented as the next steps, not claimed.