new mini blog series · multi-agent security

The Handoff Is the Soft Joint

A brand-new mini blog series. Single-agent LLM security has a playbook by now. But the systems people ship are not single agents, and the moment there are two, a new class of bug appears at the seam between them. This is a field lab and an eight-part series about that seam.

Start with A1 →

A multi-agent system is a group of models that pass work to one another. One agent triages, hands off to another that looks up your account, which hands off again to one that can issue a refund or run a query. Each pass is called a handoff, or a delegation, or an edge, depending on the framework. And each one is a decision made by a model reading text, then honored by the runtime with no authorization check behind it. That is the soft joint. It is where control and trust move between agents, and it is exactly where the bugs live.

Multi-agent systems do not fail because any single agent is dumb. They fail at the seams between agents, where each one assumes the other did the checking.

why nowSingle-agent security does not cover this

Prompt injection, jailbreaks, tool abuse, data exfiltration: the single-agent threat model is well travelled. But none of those bugs describe what goes wrong when one agent hands control to another, because in a single-agent system there is no handoff to attack. The interesting failures of the systems shipping right now are architectural, not about any one model being fooled.

single agent

One model, one context, one set of tools. The boundary is you versus the model, and the playbook for that boundary is mature.

multi-agent

Several models passing control and a shared context between them. Every pass is a trust decision nobody authorizes, and that is brand-new attack surface.

Add a second agent and you inherit a whole surface at once: a value one agent writes that another reads as authority, a routing decision an attacker can name, a retrieved document a peer trusts like a first-party instruction, a partner whose self-declared powers nobody clamps. This series walks that surface one exploit at a time.

the seamsWhere the exploits live

The eight exploits are not eight unrelated tricks. They are leaks at a handful of seams, each one an implicit trust the architecture ships with by default. Learn the seams and the individual attacks stop feeling like a grab bag:

  1. The carried context. The shared state each hop reads as memory, so a value smuggled into it is read as a decision the system made.
  2. The user message. Untrusted text treated as instructions rather than data, so control markers in a chat message get obeyed.
  3. The handoff route. The next agent is a free-form value the model emits, dispatched with no allow-list at the join.
  4. The tool call. A tool runs because text asked for it, with no per-agent capability actually enforced.
  5. Retrieval. Content a peer fetched is trusted like a first-party instruction: the indirect-injection channel.
  6. Registry and federation. A named agent, or a cross-org partner's self-declared card, is trusted without authenticating identity or authorizing capability.

the labWhat every post is built on

The whole series runs against one scenario, a bank support desk called VulnBank: three agents on a rising privilege gradient, from a user-facing triage agent that holds nothing, up to an operations agent that can move money and read files, plus a retrieval peer that turns the chain into a mesh and, later, a cross-organization partner reached over a federation boundary. It is offline, standard-library-only Python, no API keys and no network, with a single policy switch that flips the same code between three builds:

VULN
trusted
the seam is honored on faith; every attack succeeds
DETECT
filtered
an input filter bolted in front, the popular real-world answer
FIXED
authorized
structural controls at the seam; every attack blocked

You can read each post on its own, or clone the lab from GitHub and watch every attack succeed and then get blocked in your own terminal.

python run.py        # run every exploit against both builds, see the result matrix
python metrics.py    # the measured harness: block rate per policy

the methodHow each post is structured

Every part in the series follows the same shape, so you always know what you are getting: the mechanism first, then proof it works, then proof the fix works, measured rather than asserted.

  1. The payload. The exact string or move, and the seam it targets.
  2. Vulnerable and hardened traces. The same step shown succeeding, then blocked, toggled in place.
  3. A threat model. The asset, the STRIDE category, the trust boundary, the adversary's reach.
  4. The structural fix. A control that does not read the attacker's wording.
  5. A measured block rate. Not "this should help": a number from the harness.

the lensThreat modeling, in every post

This is not a list of party tricks, it is a threat model built one exploit at a time. Every post carries its own slice: the asset at risk, the STRIDE category it falls under, the trust boundary it abuses, the adversary and exactly how much reach they need, and the OWASP LLM entry it maps to. Read end to end, those slices compose into a full STRIDE view of the agent mesh and an attack tree, several independent routes to the same crown jewel, which is how you see why closing one branch never closes the goal.

per exploit

Asset, STRIDE category, trust boundary, adversary reach, and OWASP mapping, on every single page.

across the series

A STRIDE table for the whole mesh, attack trees to each crown jewel, and an audit question you can take to your own system.

the findingWhy filtering is a speed bump, not a control

The result that ties the series together is a measurement. Take the natural-language injections the attacks use, grow them into a 185-phrasing corpus across plain, obfuscated, and paraphrased forms, and score each build. An input filter, even granted flawless de-obfuscation, tops out at 43% (81 of 185) because the rest is pure paraphrase no fixed vocabulary can enumerate. The structural controls reach 100%, because they never read the wording at all.

Detection scales with the attacker's vocabulary. Prevention does not.

the defenseEight fixes, three ideas

All eight exploits, and all eight named fixes, collapse into three controls. The series earns each one attack by attack, but here is the whole argument in advance:

control 1

Data is not instructions

Never act on control content that arrived in untrusted text, carried notes, or a retrieved document. Enforced by provenance, not by pattern-matching the phrasing.

control 2

An explicit, least-privilege agent graph

Handoffs follow an allow-list, each agent holds only the capabilities it needs, the registry is authenticated, and a directed edge cannot be taken twice.

control 3

Authenticate identity and authorize capability, separately

Verify a partner's card signature, then clamp its declared powers to a local grant, because a signature proves who signed, not what they may do.

the seriesEight exploits, one seam

From a single-marker context injection, through the four-agent mesh, to the cross-organization federation boundary. Part 1 is live now; the rest release on a rolling schedule rather than all at once.

A1 Context-variable injection A single role marker smuggled into the shared context escalates a customer to admin, and the account tool hands back an SSN it should have gated. live
A2 Handoff-target coercion One marker in a user message reroutes a low-tier agent straight past its guardrail into the privileged tier, and moves money. soon
A3 Capability jumping A tier-0 agent that holds no capabilities is coerced into running a database-admin tool, dumping the whole customer table. soon
A4 Rogue agent registration No prompt injection at all: an attacker registers a malicious agent under a trusted name, and handoffs route to it. soon
A5 Indirect injection through a peer mesh The user message is completely benign. A poisoned document a peer retrieves carries the escalation, and reads another user's SSN. soon
A6 Delegation loops mesh A poisoned article bounces control between two agents until the hop budget is gone: a denial of service that costs real inference spend. soon
A7 Agent-card forgery a2a A forged partner card that merely names a trusted issuer is imported verbatim, and self-grants database-admin. soon
A8 Capability over-claim a2a A card validly signed by a trusted issuer claims a power it was never granted. A signature proves who signed, not what they may do. soon
release schedule

Part 1 (A1) is published. A2 through A8 land one at a time, following the arc from a simple chain, through the mesh (A5, A6), to the federation boundary (A7, A8). This page updates as each goes live, so bookmark it to follow along.

who this is forRead it if you build with agents

If you are wiring up an agent framework, reviewing one for security, or just trying to reason about what your multi-agent stack actually trusts, this series is written for you. You do not need the lab to follow the arguments, but it is there if you want to run every claim yourself. Each post ends with the audit question that surfaces its bug in a real system, so by the end you have a checklist, not just a story.