This playbook is the architectural capstone of agent security. Simon Willison's lethal trifecta (June 2025) names the exact combination that turns prompt injection into data theft, and Meta's Agents Rule of Two turns it into a design constraint you can enforce.
Tier: Advanced · Workflow area: RAG & agent security
Taint-tracking is the mechanism that lets an agent keep all three capabilities most of the time while blocking the dangerous moments. This playbook teaches all three accurately and applies them to redesign an unsafe agent of your own into a safe one.
Threat, briefly — the lethal trifecta. An agent is dangerous when it combines all three of: (1) access to private data, (2) exposure to untrusted content, and (3) the ability to communicate externally (exfiltrate). With all three, an injection hidden in untrusted content can instruct the agent to read private data and send it out. Remove any one leg and the attack has no path.
Classify your agent against the three legs. For the session, mark each: does it touch private/sensitive data? does it ingest untrusted content? can it externally communicate or change state? All three present = exploitable by design.
Apply the Agents Rule of Two. Meta's formulation: within a session an agent should satisfy at most two of these properties — [A] processes untrustworthy input, [B] has access to sensitive systems or private data, [C] can change state or communicate externally. If a workflow needs all three, it must not run autonomously — require human-in-the-loop or split it into separate sessions that each hold only two.
Design rule (enforce in review):
count(A_untrusted_input, B_sensitive_access, C_external_action) <= 2
If all three are required -> human approval gate OR session split.
Break a leg by design where you can. Prefer the cheapest leg to remove per workflow: strip the exfil leg (egress allow-list, no external comms — see pb-33), or the private-data leg (don't give the untrusted-input path access to secrets), or isolate untrusted content into a session that holds nothing sensitive.
Where you must keep all three, add taint-tracking. Mark any data derived from untrusted content as tainted. Propagate the taint through the agent's state. Gate privileged actions (external send, sensitive read, state change) on whether their inputs are tainted; if tainted, block or force human confirmation.
class Value:
def __init__(self, data, tainted=False):
self.data, self.tainted = data, tainted
def external_send(v: Value):
if v.tainted: # data touched untrusted content
return require_human_approval(v) # gate, don't auto-send
return send(v.data)
Make the taint gate un-bypassable from model text. Like the human gate in pb-37, the confirmation must come from the UI/session, never from a token the model can emit.
Test the trifecta on your local agent. Build the unsafe all-three version locally, land an indirect injection that reads private data and tries to exfiltrate, then apply Rule-of-Two / taint-gating and confirm the exfil is blocked.
# local agent you own
python run_agent.py --task "process this ticket" # ticket = poisoned doc
# undefended: private data beacons out. defended: tainted send BLOCKED.
The lethal trifecta names the danger; the Rule of Two makes it a countable design rule (hold at most two of untrusted-input / sensitive-access / external-action); taint-gating lets you keep all three safely only by blocking privileged actions on tainted data.
The defense holds when the redesigned agent cannot complete the read-private-then-exfiltrate chain. Success criteria:
The proof is the exfil channel closing while normal use survives — a working Rule-of-Two or taint boundary, not a blanket shutdown.
This is a design and policy engagement, ideal for client work because it produces a reusable rule rather than touching data. Run the three-leg classification and Rule-of-Two count on the client's agent architectures in a whiteboard/review setting, and encode the check into their design-review template so every future agent is scored the same way. The taint-gating and session-splitting are implemented in the client's own code; the proof-of-concept exfil test runs against a client-approved staging agent with a synthetic poisoned document and a local canary listener. The output — a before/after architecture and a standing design rule — is exactly the kind of durable, framework-aligned control (mapping to OWASP LLM01/LLM06 and the Agentic Top 10) that a client's security architecture function can own long after the engagement ends.