PASS THE OSAI CERT, Multi-Agent Part 1: Architecture and the A2A Protocol
Real deployments split work across many agents that talk to each other. This post maps the coordination patterns, the frameworks that build them, and the Agent-to-Agent (A2A) protocol that wires them together over the network.
The agents module treated one agent as the target: one model, one system prompt, one set of tools, one loop. Production systems rarely stay that small. As soon as a team wants an assistant that can research, query a database, file a ticket, and send an email, they stop cramming every tool into a single prompt and split the work across several agents, each with its own model, instructions, and privileges. Those agents then have to talk to each other.
That conversation between agents is the new attack surface this module is about. Every weakness from the agents module still applies to each agent individually, and on top of it you get a channel that most teams never think of as a trust boundary: one agent’s output becoming another agent’s input. This first post builds the model the rest of the module needs: how multi-agent systems are wired, which frameworks build them, and how the Agent-to-Agent (A2A) protocol carries the traffic when the agents live on different hosts.
A note on protocol version. This module’s A2A examples (method names, task-state values, the Agent Card’s
urlfield) target spec v0.3, the line this module was written against. A2A reached v1.0 in early 2026 with breaking wire-format changes:message/sendbecameSendMessage, task states moved to aTASK_STATE_*enum, and the Agent Card’surlfield was replaced by asupportedInterfaceslist. The attacks in this module (enumeration, orchestrator bypass, identity spoofing, data poisoning) target the trust model, not specific field names, so they carry over to v1.0 deployments unchanged. Confirm the wire details against whatever version your target actually runs before reproducing an example verbatim; see the official migration notes for the full list of changes.
Why teams split one agent into many
The reasons are practical, and each one has a security consequence worth noting.
- Specialization. A narrow agent with a short prompt and three tools follows instructions better than a generalist juggling thirty. The consequence: capabilities get concentrated. One agent ends up holding the database, another holds email, and the dangerous tool is now a specific node you can locate.
- Context limits. Splitting work keeps each context window small. The consequence: agents hand each other summaries and results instead of raw context, and every hand-off is a place where text written by someone else enters a prompt.
- Different owners. The research agent belongs to one team, the billing agent to another, and a third comes from a vendor. The consequence: nobody owns the whole trust chain, and each team assumes the agent upstream of them did the validation.
- Least privilege, in theory. Giving each agent only the tools it needs should contain a compromise. In practice it only works if the agents don’t simply ask each other to do things, which is exactly what they are built to do.
Keep the structural flaw from Agents Part 1 in mind: a model reads everything in its context as one flat trust space. In a multi-agent system that flat space spans agents. A worker’s result lands in the orchestrator’s context with the same authority as the developer’s instructions, and a worker reads a delegated task as an order from a trusted colleague. Treat agent output as tool output with a better reputation.
Coordination patterns
How the agents are wired decides where your input travels and which agent finally acts on it. Most systems are one of these patterns, or a nesting of several.
Orchestrator and workers (supervisor). A central agent receives the user’s request, decides which specialist should handle each piece, delegates, and assembles the final answer from the workers’ results. It’s the most common shape, and A2A’s own samples use it (they call the orchestrator the host agent). For the attacker, the orchestrator is two things at once: a router whose routing decision is an LLM reading worker descriptions, so the decision itself is injectable, and the single place where every worker’s untrusted output gets read together. Workers, for their part, usually trust anything that arrives as a delegation.
Sequential pipeline. Agent A’s output is agent B’s input, B’s is C’s, and so on: an intake agent, then a classifier, then a drafting agent, then a sending agent. An injection at stage one rides the pipeline to the last stage, and the last stage is often the one with the side effect (send, publish, commit). Each stage that summarizes can mangle your payload, but each stage that passes data through untouched launders it into “output of an internal agent.”
Hierarchical teams. Supervisors of supervisors: a top-level planner delegates to team leads, which delegate to workers. It’s the orchestrator pattern nested, which means longer trust chains and more summarization layers between your input and the agent that acts.
Handoff and swarm (peer to peer). There’s no central router. The active agent
decides who takes over next and transfers control, often through a generated tool
like transfer_to_billing_agent. The attack here is steering the handoff: an injection
that convinces the current agent to pass control, along with the conversation, to the
agent that holds the dangerous tool.
Group chat and shared blackboard. All agents read and write a shared transcript or shared state object, and a selector (an LLM or a fixed rotation) picks who speaks next. One poisoned message in the shared space is read by every agent in the group, which makes this the pattern where an injection spreads the fastest.
Two variations show up inside all of these. Fan-out and fan-in sends the same task to several workers in parallel and merges the results, so a single poisoned result can win the merge. Critic or verifier loops have one agent check another’s work, and that verifier is frequently the only guardrail in the system, which means the guardrail is itself an LLM reading attacker-influenced text.

Whatever the pattern, ask the same three questions of it. Who decides routing, and is that decision made by a model reading text you can influence? Which agent reads whose output? And how many hops separate your input from the agent holding the most dangerous capability? The answers become your attack paths.
Common frameworks
Most multi-agent systems are built on one of a handful of frameworks. You don’t need to know their APIs in depth, but you should recognize them, because each one leaks recognizable artifacts (package names, generated tool names, default ports) and each one has a default trust behavior worth testing.
| Framework | Coordination model | Recon tells | Where trust leaks |
|---|---|---|---|
| LangGraph | Graph of nodes with shared state; prebuilt supervisor and swarm | langgraph in manifests, transfer_to_<agent> handoff tools, langgraph dev API server on port 2024 |
Shared graph state is read by every node |
| CrewAI | Role-based crews, sequential or hierarchical (manager agent) | crewai package, role/goal/backstory prompt scaffolding, “Delegate work to coworker” tool |
Delegation passes free-text tasks between agents |
| AutoGen / Microsoft Agent Framework | Group chats (round robin, selector, swarm) | autogen_agentchat imports, speaker-selection prompts in traces |
Every agent reads the whole group transcript |
| OpenAI Agents SDK | Handoffs and agents-as-tools | openai-agents package, transfer_to_<agent> tools |
Handoff carries the full conversation to the next agent |
| Google ADK | LLM delegation (sub_agents) plus sequential, parallel, and loop agents |
google-adk package, transfer_to_agent calls, dev server on port 8000 |
Native A2A exposure turns in-process agents into network services |
Notice one split that matters more than the framework name: whether the agents live in one process or on separate hosts. Inside one process, agents call each other as functions, and the only way to attack the inter-agent channel is by getting text into it, through injection. When the agents are separate services, owned by different teams or written in different languages, they need a wire protocol. That is where agents become network endpoints you can scan, call directly, and impersonate, and it’s where A2A comes in.
The A2A protocol
Agent2Agent (A2A) is an open protocol, announced by Google in April 2025 and now a Linux Foundation project, for letting agents built on different frameworks discover each other and exchange tasks over HTTP. It complements MCP rather than competing with it, and the two sit at different layers: MCP connects one agent down to its tools and data (the MCP post covered that channel), while A2A connects an agent across to its peers. A system often runs both at once, an agent speaking MCP to its own tools and A2A to the other agents, so in practice you enumerate and attack each layer separately. A key design choice is that remote agents are opaque: the caller doesn’t see the remote agent’s prompt, tools, or memory, only what it advertises and what it returns. The caller is trusting a black box, and that’s precisely the property this module exploits.
A note on versions before the details. The protocol has moved fast. This module follows
the 0.3 line of the spec; older deployments serve the discovery document at
/.well-known/agent.json instead of /.well-known/agent-card.json and use method names
like tasks/send instead of message/send. The card’s protocolVersion field tells you
which dialect a target speaks, so read it before you send anything.
The Agent Card
Every A2A server describes itself with an Agent Card, a JSON document served at a well-known path. It’s the agent’s business card: who it is, where to send requests, how to authenticate, and what it can do.
{
"protocolVersion": "0.3.0",
"name": "Inventory Agent",
"description": "Answers stock and pricing questions by querying the inventory database.",
"url": "http://inventory-agent.corp.local:10001/",
"preferredTransport": "JSONRPC",
"version": "1.4.0",
"capabilities": { "streaming": true, "pushNotifications": true, "stateTransitionHistory": false },
"securitySchemes": {
"bearer": { "type": "http", "scheme": "bearer" }
},
"security": [{ "bearer": [] }],
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain", "application/json"],
"skills": [
{
"id": "stock_lookup",
"name": "Stock lookup",
"description": "Run SQL queries against the inventory database to answer stock questions.",
"tags": ["inventory", "sql"],
"examples": ["How many units of SKU-4471 are in the Madrid warehouse?"]
}
],
"supportsAuthenticatedExtendedCard": true
}
Read it the way an orchestrator does, and three attacker facts jump out. First, the card
is self-asserted: the agent says what it is, and nothing in the base protocol proves
it. The 0.3 spec added optional JWS signatures, but a client has to verify them for
that to matter, and many don’t. Second, the description and skill text are natural
language that ends up in a prompt: an LLM-driven orchestrator reads them to decide
where to route a task, so the card is an injection channel aimed at the orchestrator.
Third, url is where every subsequent request goes. The client fetches the card
once and then talks to whatever address the card names, a detail Part 5 turns into a
persistent hijack.
Messages, tasks, and artifacts
The default transport is JSON-RPC 2.0 over HTTP POST to the card’s url; 0.3 also
defines gRPC and a REST-style HTTP+JSON binding. The core methods are few:
message/sendsends a message and gets back either a direct reply or a task.message/streamdoes the same over Server-Sent Events, for incremental updates.tasks/getandtasks/cancelread or stop an existing task by its id.tasks/resubscribereattaches a dropped SSE stream to a task already in flight.tasks/pushNotificationConfig/setregisters a webhook URL the server will call with task updates, for long-running work.
A message has a role (user for the caller, agent for the remote agent) and a
list of parts. A part is one of three kinds: a text part, a file part (inline
base64 bytes or a URI), or a data part carrying arbitrary structured JSON. Work that
takes time becomes a task with an id and a lifecycle (submitted, working,
input-required, completed, failed, canceled, rejected, auth-required), and a
finished task carries its results as artifacts, which are made of the same three part
kinds. A contextId groups related tasks and messages into one conversation. Messages
and parts also carry an optional metadata object, and since some agents fold that
metadata into the prompt too, it’s another text channel to test, not just the parts.
Here is the whole round trip by hand. This is the request an orchestrator makes when it delegates to the inventory agent:
curl -s http://inventory-agent.corp.local:10001/ \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"messageId": "6f1c2a0e-1d7b-4e55-9a51-2c8f0d3b7e11",
"parts": [{ "kind": "text", "text": "How many units of SKU-4471 are in stock?" }]
}
}
}'
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"kind": "task",
"id": "3b9d6c1e-58f4-4a8e-b0a2-7c6e5f1d9a40",
"contextId": "a71e0c55-2f3d-4b9e-8e61-0d4c7b2a9f13",
"status": { "state": "completed" },
"artifacts": [
{
"artifactId": "c2f8e9b4-7a16-4d3e-9b05-1e6a4c8d2f77",
"parts": [{ "kind": "text", "text": "SKU-4471: 214 units in Madrid, 37 in Lisbon." }]
}
]
}
}
That artifact text is what the orchestrator reads next, as context, at full trust.
Authentication: declared versus enforced
A2A deliberately leaves identity to the transport. The card declares schemes in
securitySchemes (API key, HTTP bearer, OAuth 2.0, OpenID Connect, mutual TLS), clients
obtain credentials out of band, and the server is supposed to authenticate every
request. Two gaps follow, and both show up constantly on internal deployments. The card
can declare a scheme the server never checks, so “the card says bearer” is a claim to
test, not a fact. And transport identity says which caller sent a request, not whether
the content is trustworthy: an orchestrator with a valid token can still forward your
injected text to every agent it calls.

The attacker’s map of a multi-agent system
Pull those pieces together and a multi-agent system offers four kinds of surface, which is also the outline of this module:
- Discovery metadata. Agent Cards, registries, and API schemas tell you every agent, skill, endpoint, and auth requirement. They’re also prompts the orchestrator reads. Part 2 enumerates them.
- Endpoints. Each agent is an HTTP service with ordinary web bugs: missing auth, IDOR on task ids, a webhook that fetches arbitrary URLs. Part 3 attacks them directly.
- The workflow. Outputs flow from agent to agent and finally to a human or a database. Part 4 abuses that flow to plant malicious links and reach command execution through a SQL agent.
- Identity. How the orchestrator decides which agents exist and that an agent is who it claims to be. Part 5 registers a rogue agent and spoofs a legitimate one through DNS.
Part 6 closes with the technique that needs none of the above: poisoning the data that flows through the system so the payload enters as trusted internal data and hops from agent to agent.
A short glossary
The module reuses these terms, so pin them down once:
- Host agent / orchestrator. The agent that receives the user’s request and delegates to others. A2A’s samples call it the host agent; this series mostly says orchestrator.
- Worker / remote agent. A specialist the orchestrator calls. Opaque to its caller: you see its card and its answers, not its prompt or tools.
- Agent Card. The self-asserted JSON description an A2A server serves at
/.well-known/agent-card.json: name,url, skills, auth schemes, capabilities. - Skill. One advertised capability on a card (id, description, examples). The closest thing to a tool list you get for a remote agent.
- Message / part. What a caller sends: a message with a
roleand a list of parts, each parttext,file, ordata. - Task. A unit of work with an id and a lifecycle state, created when the answer isn’t immediate.
- Artifact. A task’s output, built from the same part kinds as a message.
- contextId. The id that groups messages and tasks into one conversation.
Where this is going
You now know the shapes multi-agent systems take, the frameworks behind them, and the A2A messages that carry their traffic. The next step is the same one the recon module taught: find all of it before you attack any of it.
Continue to Part 2: Enumerating A2A Workflows →
This is an independent study guide. DSEC Labs is not affiliated with or endorsed by OffSec. Only run these techniques against systems you are authorized to test. DSEC Labs does this work professionally as AI red teaming.