A1 through A3 all abused the same thing from different angles: an LLM acting on untrusted text. A4 is the outlier that proves the surface is bigger than the prompt. It sends no markers, needs no injection, and never speaks to the model. It attacks the layer beneath the agents: the registry that maps a name like OpsAgent to the code that runs when someone hands off to it. Poison that map and every legitimate handoff becomes an attacker's entry point.
What the buttons do: click VULN and every console below shows the attack succeeding on the vulnerable build; click FIXED and the same consoles show it blocked on the hardened build. It switches in place: nothing reloads, so you can flip back and forth and compare the exact same step exploited vs. defended.
the seamBoundary ❾ — the registry
Agents are resolved by name. A handoff to OpsAgent is really a lookup: "find the agent registered under that name, run it." The registry is trusted implicitly: nobody re-checks, at handoff time, that the OpsAgent now in the table is the same OpsAgent the operator deployed. If registration is open, an attacker can register their own agent under an existing trusted name and inherit all the trust that name carries. It is DNS cache poisoning for agents.
the payloadOverride the name
The attacker registers a malicious agent under the name OpsAgent, overriding the real one, so the next legitimate handoff routes into their code and reads secrets.env. The fix is an authenticated registry that refuses to override an existing name without an operator token:
[attacker] register rogue 'OpsAgent': ok=True [AccountAgent] handoff -> OpsAgent: ok # now the attacker's agent [OpsAgent] tool read_file('secrets.env') -> OPENAI_API_KEY=sk-REDACTED-LIVE-KEY
[attacker] register rogue 'OpsAgent': ok=False (DENIED: cannot override existing agent) [AccountAgent] handoff -> OpsAgent: ok # the REAL OpsAgent [OpsAgent] tool issue_refund('u1001','100') -> REFUND_ISSUED account=u1001 amount=100 # benign
the fixAuthenticate the registry
Registration is a privileged operation, so it must be authenticated. Overriding an already-registered name requires an operator token; without it, the override is refused and the deployed agent stands:
if self.policy.authed_registry and name in self.agents: if not operator_token_valid(token): return Denied("cannot override existing agent") # first registration is fine; overriding a trusted name is not
A4 forges an internal identity; A7 forges a cross-organization one. The mistake is identical: trusting a name without verifying who actually stands behind it. But the trust anchor differs: A4's is an operator token you hold internally, A7's is a cryptographic signature from an issuer you chose to trust. Same lesson, two boundaries.
threat modelA4, seen from above
| Asset at risk | secrets.env: API keys and the DB password. This is the only exploit in the set that reaches the secrets file directly. Property lost: confidentiality (and, downstream, everything those keys unlock). |
| STRIDE category | SSpoofing: the attacker impersonates a privileged agent, which then yields IInformation disclosure of the secrets. |
| Trust boundary | ❾ the registry / federation boundary, where an agent identity is admitted to the mesh. |
| Adversary & reach | An insider or a weak registration path: anyone who can write to the registry. Note the shift: unlike A1–A3, the channel is not the chat message; it is the control plane. |
| Attack-tree branch | A4 is a goal in its own right (exfiltrate the secrets file), orthogonal to the SSN tree, which is exactly why a threat model must enumerate assets, not just attacks. |
deep diveThe adversary you forget: not everyone comes through the chat box
A4 is the reason the series' threat model lists adversaries by the channel they control, not by a single "the attacker" abstraction. The important, counter-intuitive thing about the full list is that none of these adversaries need to touch the model; each just needs a way to get bytes into a surface the runtime reads. A4 is the one whose surface is the control plane rather than the conversation.
| Adversary | Channel they control | Boundary | Exploits |
|---|---|---|---|
| External user | the chat message | ❺ | A1 · A2 · A3 |
| Poisoned knowledge source | a retrieved document; no direct access needed | ❽ | A5 · A6 |
| Malicious / over-eager partner org | its own Agent Card | ❾ | A7 · A8 |
| Insider / weak registration path | the agent registry | ❾ | A4 |
Threat-model a mesh only through the prompt and you will build a beautiful input filter and still ship A4 wide open. The registry is a trust boundary; treat writes to it like writes to production config, because that is what they are.
in the wildWhere A4's seam lives in real frameworks
Service discovery / agent registries
Any system where agents are resolved by name at runtime has this seam. Require authenticated registration and treat name-override as a privileged, audited event, never a silent upsert.
MCP server configuration
An MCP server is a named capability provider. Pin which servers a client trusts; don't let an untrusted process register or shadow a server name that tools resolve against.
Plugin / tool marketplaces
This is OWASP LLM03 (Supply Chain): a component admitted by name inherits trust. Verify provenance at install and at resolve time, not just once.
Every registration and every override is a security event: log it with the identity that made the change and alert on any override of an existing agent name. A deployed agent's implementation hash changing without a deploy is your tripwire.