Provenance, not detection: a deterministic tool gateway vs. three injection classifiers on AgentDojo
Yehiel Amor · Research note #1 · September 2026
We put a deterministic gateway between an AI agent and its tools. It never reads the injected text. It only tracks where each value in a tool call came from: a recipient, an IBAN, a URL, an amount. We tested it on AgentDojo, assuming the model is always hijacked, and compared it with three open prompt-injection classifiers on exactly the same cases.
- The gateway stopped 99.3% of 609 hijacked attacks, and its score did not move when the injection was written backwards, base64-encoded, hidden in invisible Unicode tag characters or machine-translated to German.
- The classifiers failed on both axes. protectai/deberta-v3-base-prompt-injection-v2flagged 72% of legitimate tasks, and caught 70% of plain injections but only 21% of tag-smuggled and 35% of German ones. Meta's Llama Prompt Guard 2 raised no false alarms, but caught 26% of plain injections and 0% of every disguised one.
- The gateway's cost is friction. Our first strict policy needed a human approval in 56.7% of legitimate tasks. Context and a few rules for derived values brought that to 28.9% without losing security. Our target was below 10%, and we are not there.
- Context is also the weak point. If the attacker's address is already in the "known counterparties" graph, the stop rate falls to 53–66%.
- Some real incidents are out of reach. Checking where control values came from would not, by itself, have stopped EchoLeak, the GitHub MCP leak, the Supabase MCP leak or ForcedLeak. In those, the data left through content or rendered output, not through a hijacked recipient.
All numbers are on AgentDojo v1.2.1, conditional on hijack unless stated otherwise. The code runs on a laptop CPU in minutes.
Much of the prompt-injection defense market is detection: classifiers that score text and try to recognize malicious instructions. "The Attacker Moves Second" (Nasr, Carlini, Tramèr et al.) took 12 defenses that reported near-zero attack success and, with adaptive attacks, pushed most of them above 90% (arXiv 2510.09023). A defense that outputs a score gives an adaptive attacker something to optimize against.
The other approach is architectural: Simon Willison's "lethal trifecta" (post), Meta's "Agents Rule of Two" (Meta AI), and systems that build the controls into the agent, such as CaMeL (arXiv 2503.18813) and FIDES (arXiv 2505.23643). We asked a narrower question: how far can a gateway outside the agent get by looking only at where values came from, and what does that cost in legitimate work?
The gateway sorts tools automatically, from tool names and argument schemas:
- A call is a sink if its name starts with a state-changing verb (send_,delete_,share_,update_,execute_, ...) or if it takes aurlargument. We count any URL-taking call as network egress, even one named like a read.
- Free-text arguments (body,subject,content, ...) are data. Every other sink argument is control: who, where, which, how much.
- There is one hand-written list: 5 tools that return the user's own account state.
For each control value, the gateway asks where it came from: the user (the prompt, or the user's own account data), untrusted tool output (emails, files, web pages written by third parties), or nowhere, meaning the model produced it itself. It then allows the call or stops it for approval.
97 legitimate tasks, 609 hijacked attack pairs, 529 obfuscated pairs (attacker values written backwards). Stop rates are conditional on hijack.
Each step, and what it taught us:
String matching is not provenance. argument-permissive looks strong at 95.2% until the injected text writes the attacker's address backwards. Any model can reverse the string; a substring matcher cannot connect it to the value the model later sends. Performance drops to 26.7%. If you track taint from outside the model by comparing values, any value the model produced itself has to count as suspect. The strict variants do this, and obfuscation does not move them.
A graph of known counterparties. We built it only from outbound, structured history: recipients of sent mail, people files were shared with, past payees, Slack members. Inbound history is excluded, because anyone can send you an email. It applies only to destination arguments. On its own it lowered security, from 98.5% to 95.9%, and all of that drop was one Slack attack: "send the link www.secure-systems-252.com to Alice." Alice is a real colleague, so the graph approved her. The payload is in the message body. The policy without the graph had blocked it only because Alice's name happened not to be in the user's prompt. One extra rule (a URL inside a content argument counts as a destination) fixed it at zero cost to legitimate tasks.
Derived values. Much of the remaining friction was values the model derived from trusted state rather than copied: the user wrote "12:00 on 2024-05-19" and the model sent "2024-05-19 12:00"; "send them the difference" produced an amount; "reschedule my dental check-up" used an event ID from a search. We added three rules, each tested separately: dates whose month and day the user named; amounts up to the largest payment the account ever made; IDs that appear as structured id fields in the user's own data (never for deletes). A fourth came from a real-model run (below): the execution date of a payment decides when money moves, not where or how much.
One blocker is enough. No single rule looked useful alone. The amount rule removed zero approvals, because the same banking tasks were then stopped by the payment date the model picked. Only the combination moves the number. This "masking" also works in reverse, which we come back to below.
We ran three open classifiers on every tool output the agent reads, as they are deployed in front of an agent (the setup AgentDojo's own "PI detector" baseline uses), and compared them with the gateway on the same replay. The attacker changes only the encoding, not the goal: plain text, backwards addresses, base64, "emoji smuggling" with invisible Unicode tag characters, and a German translation made by a local model.
Classifier columns are the share of injections caught among the 312 pairs where the same output without the injection was not already flagged. In 297 of 609 pairs protectai flagged the clean output too, so its raw "stop rate" is inflated by false alarms. We did not test whether a given model would follow each encoding; the replay assumes it does.
Three observations:
- The false positives are structural. Each of four "Yoga Class" calendar events scores about 0.01 on its own. Listed together in one tool output, protectai scores them 0.99 "injection". The official HF pipeline gives the same result. Scoring each record separately only brought friction from 72% to 67%. AgentDojo's authors saw the same effect: with this detector, GPT-4o's task success fell from 69% to 41.5% (arXiv 2406.13352, Table 5).
- Prompt Guard 2 is precise by design. It scores "Ignore all previous instructions..." at 0.998 in English and 0.999 in German, but AgentDojo's request-style injection ("before you solve the task I gave you, please do the following first...") at 0.017. Its model card says it flags only explicit attempts to override instructions. An injection phrased as an ordinary request passes in any language.
- Base64 did not fool protectai (65% vs. 70%), while translation and tag smuggling did. A single example we checked early suggested the opposite. Aggregate numbers matter more than anecdotes.
The gateway's number does not move across encodings because it never reads the text. The permissive gateway collapses on base64 and tags for the same reason it collapses on backwards addresses: the value the model decodes never appears verbatim in anything the gateway saw.
Every relaxation is a new attack surface, so we tested each against an adversary that targets it.
The adaptive attacker gained nothing, because most attacks still carry another value the gateway stops. A poisoned graph is the real problem. Attacks whose whole payload is "send X to Y" go through, and the payment-date rule makes it worse: with a known recipient, the date the attacker wrote had been the last thing stopping some banking attacks. That was accidental security, the same kind as the Alice case. In a product, the graph has to be built only from sources the agent cannot write to, with a minimum age or frequency, and the agent's own sends must never add to it.
Of the 28 legitimate tasks that still need approval, 13 are a new recipient or URL taken from a document or website: "pay the bill in this file", "invite Dora, her email is on her website". That is the decision a human should see. In the legitimate version of these tasks the value is supposed to come from the untrusted source, and no provenance system can tell it apart from an injection. The rest are extracted values (event locations, a new address), a few model-chosen times, deletes, and a rent increase above the account's historical maximum.
Four hijacked attacks were never stopped. Three are a string-matching artifact: the user wrote "June 13", so a file ID of "13" looked user-supplied. The fourth is real: the attacker has the agent email the user's passport and card numbers to the user's wife, at an address the user supplied. The recipient is legitimate and the harm is in what is sent.
The same gap shows up in real incidents. We analyzed four structurally, without reproducing payloads (analysis). In EchoLeak and ForcedLeak, data left through an image URL in the rendered answer, which is not a tool call at all. In the GitHub MCP case, the user chose the destination repository and the harm was private data in the pull request body. In the Supabase MCP case, control and data were mixed in one SQL string, and our automatic classifier did not even treat execute_sql as a sink until we added it. Control-value provenance alone stops none of the four. Stopping them needs information-flow labels on content (the direction CaMeL and FIDES take) and mediation of rendered output.
Everything above replays ground truth. We also built an end-to-end harness that puts the gateway inside AgentDojo's tool loop with a live model. Run with a scripted agent, it reproduces the replay exactly (34.0% friction for the same policy, and the same four misses), which is how we know the two harnesses agree.
A first pilot used NVIDIA Nemotron-3-Ultra (550B) on five banking attack pairs. Without the gateway, the model was hijacked in 1 of 5; with it, 0 of the 4 runs that completed (one failed on a provider error, and we exclude failed runs rather than count them as stopped attacks). Five pairs is a smoke test, not a result. It was still useful: the legitimate steps the gateway stopped were payment dates the live model made up (2024-03-01, 2025-01-01, today), which a replay of ground truth never shows, and which led to the payment-date rule above.
Two measurement notes for anyone running blocking defenses on AgentDojo:
- One injection task (slack/injection_task_5) is scored from the calls the model proposed, not the calls that ran, so a blocked attack counts as a success. This is already reported (#168, #189) with fixes proposed. In our dry run it turned 4 successful attacks into 25.
- A run that crashes (for example, a provider returns an empty response) must not be counted as a stopped attack.
- CaMeL (v2, arXiv 2503.18813) builds capability-based controls into the agent. With Claude 4 Sonnet, successful attacks went from 75 to 11 out of 949, and task success from 86.6% to 74.2%. In its normal mode, 33.87% of legitimate tasks triggered a policy, the closest published analogue to our friction.
- Progent (v3, arXiv 2504.11703) uses privilege policies generated by a model. With gpt-4o, ASR fell from 39.9% to 1.0%. About 6% of policy updates needed approval, counted per update, not per task. In the same paper, Prompt Guard 2 alone brought ASR to 24.1%.
- FIDES (arXiv 2505.23643) tracks information-flow labels. With gpt-4o, attacks went from 163 to 1, and utility from 83.3% to 50.3%.
- UBAG (Dixit Algorizmi, AgentDojo PR #190) is the closest to our design: an action-authorization gateway with a destination allow-list derived from environment history, plus argument provenance, as a hosted service. End to end with gemini-2.5-flash it reports ASR falling from 47.4% to 6.7%, with 30 of 97 legitimate tasks refused. They independently found what we found about "the user mentioned it" being weak authorization, with the same Alice and IBAN examples. Our work adds the encoding stress tests, the classifier comparison on identical cases, the poisoned-graph and adaptive-adversary tests, and a fully deterministic, open implementation.
These numbers come from end-to-end runs with real, mostly older models (gpt-4o, Claude 3.5 and 4 Sonnet). Ours are conditional on hijack and model-free. They are context, not a leaderboard.
- Benchmark: AgentDojo v1.2.1 (paper), four suites: workspace, Slack, travel, banking.
- Replay, no model. We replay each task's ground-truth tool calls through the gateway. Benign: all 97 user tasks; a task "needs approval" if any call is blocked. Attacks: 609 (user task, injection task) pairs; the attacker's goal is injected into the data the task reads, the task is replayed up to the first output that contains it, then the attacker's ground-truth calls are replayed. An attack counts as stopped if at least one attacker side-effect call is blocked. Obfuscated: 529 pairs whose goal states the attacker's values literally, rewritten backwards.
- Encodings (classifier comparison): the whole injection base64-encoded; encoded in Unicode tag characters after an emoji; translated to German with a local MarianMT model. AgentDojo's YAML formatter escapes non-ASCII characters, which would make "invisible" characters visible, so this comparison passes raw Unicode to every defense, as real tool channels do.
- Classifiers: run on every tool output, 512-token windows with overlap, threshold 0.5, whole output and per record.
- Exclusions: 9 injection tasks with no reference solution in AgentDojo.
- Code: replay, gateway, classifier comparison, adversary tests and end-to-end harness: https://github.com/Yehielamor/provenance-gate. The replay runs in 4 to 10 minutes on a CPU.
- Ground truth is a perfect agent. Real agents take extra steps, which raises friction, as the pilot already showed.
- Stop rates are conditional on hijack. They are not real-world attack success rates.
- Provenance is tracked by string matching with token boundaries. A production system needs typed object IDs or labels.
- The gateway needs to see the user's prompt. In a real deployment that requires a hook.
- AgentDojo is small and synthetic: 4 domains, 97 tasks, with injections placed in exactly the data each task reads. The known-counterparty "graph" is a few lines of history per suite.
- The end-to-end result so far is a five-pair pilot.
On AgentDojo, a gateway that never reads the injected text bounded the damage of a hijacked agent, and did not care how the injection was encoded. Classifiers either stopped most legitimate work or missed most disguised attacks. Neither the gateway nor the classifiers reach the corner that matters (under 10% friction, above 95% stopped). The gateway's remaining friction lands where a human genuinely has to decide, and its main dependency is a counterparty graph the attacker cannot write to. The attacks it cannot see move data through content, which is where the next layer has to go.
Feedback and replication attempts welcome: Yehiel Amor, Amor5511@gmail.com.