What A2A Is, and Is Not
A2A, short for Agent-to-Agent, provides a vocabulary for one Agent service to ask another Agent service to perform work. Google announced the protocol publicly in 2025, and it has since evolved through an open ecosystem. Treat the specification version, transport binding, SDK, and governance location as deploy-time dependencies, not timeless facts.
A2A can help with:
- capability discovery through an Agent Card;
- structured messages and artifacts;
- short or long-running task lifecycle;
- status updates and, where supported, streaming or push delivery;
- delegation across independently operated Agent services.
It does not provide business authorization, tenant isolation, a trusted directory, safe tool execution, accurate model output, or exactly-once side effects. Those remain application responsibilities.
Choose the Boundary Before the Protocol
Use a local function when the work is owned by one service and has a narrow deterministic contract. Consider A2A when the remote service has an independently managed lifecycle, policy, or capability boundary.
| Question | A useful answer |
|---|---|
| Who owns the task state? | One named service with durable storage and retention policy. |
| Who authenticates the caller? | The remote service, using a trusted identity mechanism. |
| Who authorizes data and effects? | The service that owns the object and action. |
| What is the retry identity? | An application-generated idempotency key, not a model phrase. |
| What happens on disconnection? | A documented polling, callback, resume, or cancellation contract. |
| What evidence is retained? | Redacted lifecycle events, policy decisions, artifact references, and error classes. |
Do not introduce an Agent protocol merely to make a linear workflow appear autonomous.
A2A and MCP
The protocols address different boundaries, but the familiar “A2A is Agent-to-Agent; MCP is Agent-to-tool” shorthand is incomplete.
| Boundary | Typical purpose | Still required outside the protocol |
|---|---|---|
| A2A | delegate a task to a remote Agent service | service identity, task/object authorization, budgets |
| MCP | expose tools, resources, and prompts to an AI application | user identity, object authorization, side-effect policy |
| application workflow | coordinate local and remote work | approval, compensation, audit, business invariants |
An A2A delegate may use MCP internally; an MCP Host may call a remote service without A2A. Neither arrangement grants the model permission to read an object or perform an external write.
Pin the Specification Surface
Before implementing, record the exact inputs you have tested:
{
"a2a_specification": "pinned-release-or-commit",
"transport_binding": "pinned-profile",
"agent_card_location": "deployment-defined",
"sdk": "package-and-version",
"authentication": "deployment-specific",
"task_retention": "policy-reference",
"artifact_store": "approved-store",
"checked_at": "recorded-time"
}
The Agent Card schema, discovery location, supported transports, streaming behavior, and SDK APIs can change. Verify them against the pinned specification and your running endpoint. Avoid copying a blog’s package name into production.
Agent Cards Are Discovery Metadata
An Agent Card may describe a service name, endpoint, skills, input/output modes, and capabilities. It is useful for routing, but it is untrusted until your system establishes provenance.
Validate:
- the transport endpoint against an allowlist or trusted registry;
- service identity, issuer, audience, expiry, and key rotation where tokens are used;
- card schema and version;
- the requested skill against local policy;
- the caller, tenant, and target object for each task;
- response size, artifact type, and callback destination.
Descriptions, skill names, URLs, and examples can contain prompt injection or misleading claims. Never turn card text into executable policy, automatic installation, or tool permission.
Task Lifecycle Is a State Machine
The specification uses task and status concepts. Exact status names and legal transitions are version-dependent; a production service should define its own durable state machine around the pinned protocol.
accepted -> running -> waiting_for_input -> running -> terminal
| |
+-> canceled +-> succeeded | failed | rejected
Record both a protocol-facing status and an application status. A terminal response should say whether the task was rejected by policy, failed before effect, failed after an uncertain effect, or completed with evidence.
Idempotency and Cancellation
Retries, duplicate messages, timeouts, and callbacks are ordinary distributed-systems behavior. For an effectful task:
- bind an idempotency key to principal, tenant, action, and normalized parameters;
- persist the decision before performing the effect;
- make cancellation cooperative and state-aware;
- return an “unknown outcome” class when the effect cannot be proven;
- use compensation only where the business operation supports it.
Do not convert a model refusal or a completed label into proof that a payment, deletion, or notification happened exactly once.
Messages, Artifacts, and Untrusted Content
Message parts and artifacts may carry text, structured data, files, references, or URLs. Treat all of them as untrusted input, including content emitted by another Agent.
Bound:
- bytes, item counts, nesting depth, and decompression;
- MIME allowlists and parser behavior;
- artifact storage location, expiry, access scope, and deletion;
- outbound fetch destinations and redirect count;
- telemetry fields and raw payload retention.
Prefer reference-based artifacts with authenticated retrieval over embedding large sensitive content into task messages. A file reference is not access permission; validate it at retrieval time.
Streaming and Push Delivery
Streaming and asynchronous notifications are delivery choices, not a substitute for task durability. Whether a version/binding uses server-sent events, a callback, polling, or another mechanism, define:
- reconnect and resume semantics;
- event ordering and deduplication;
- token expiry and reauthentication;
- callback registration and destination allowlists;
- timeout, backpressure, and result-size budgets;
- who can observe or cancel a task.
Treat callback URLs as an SSRF boundary. Verify destination ownership and do not let a model or remote Agent choose an unrestricted callback target.
Authorization Is Per Task and Per Object
OAuth, bearer tokens, mTLS, workload identity, RBAC, and ABAC are possible deployment controls. They are not all mandatory A2A features, and an authenticated caller is not automatically allowed to perform every skill.
from dataclasses import dataclass
from enum import Enum
from typing import Callable
class Verdict(str, Enum):
ALLOW = "allow"
DENY = "deny"
CONFIRM = "confirm"
@dataclass(frozen=True)
class TaskRequest:
skill: str
object_id: str | None
@dataclass(frozen=True)
class AuthenticatedContext:
tenant_id: str
principal_id: str
def authorize(
context: AuthenticatedContext,
request: TaskRequest,
*,
allowed_skills: set[str],
can_access_object: Callable[[str, str, str], bool],
effect_class: str,
) -> Verdict:
if request.skill not in allowed_skills:
return Verdict.DENY
if request.object_id and not can_access_object(
context.tenant_id, context.principal_id, request.object_id
):
return Verdict.DENY
if effect_class in {"external_write", "destructive"}:
return Verdict.CONFIRM
return Verdict.ALLOW
This is an application policy fragment, not an A2A SDK implementation. The trusted caller context must come from authentication middleware, never from model-generated task fields. effect_class should come from a server-side skill registry, not from the request body; a real CONFIRM result also needs an explicit approval and audit path.
A Minimal Production Architecture
caller
-> authenticate and authorize
-> validate task shape and budget
-> persist idempotency + lifecycle event
-> remote A2A service
-> independently authenticate, authorize, and validate
-> use bounded local tools and data access
-> store artifacts in an approved scoped store
-> collect redacted result evidence
-> apply local approval / compensation / user response
Each service should recheck policy rather than trusting upstream enforcement. Cross-tenant tests must prove that a valid token and task identifier from tenant A cannot reveal or modify tenant B data.
Test the Failure Paths
| Test | What it should prove |
|---|---|
| card validation | unknown issuer, endpoint, schema revision, and skill are rejected |
| task contract | malformed parts, oversized artifacts, and unsupported modes fail safely |
| authorization | tenant, object, and action policy is enforced independently of descriptions |
| lifecycle | duplicate delivery, restart, timeout, cancellation, and late callback are deterministic |
| side effects | retries do not duplicate effect; uncertainty is surfaced |
| streaming | reconnect, replay, ordering, expiry, and backpressure have bounded behavior |
| abuse | injection, poisoned artifacts, SSRF callbacks, and artifact exfiltration are blocked |
| operations | latency, queue depth, task cost, retention, and deletion are observable |
Use deterministic oracles for policy and effects. A language-model judge can review answer quality, but cannot prove authorization, provenance, or financial correctness.
Common Failure Modes
- treating an Agent Card, registry listing, badge, or SDK adapter as a trust decision;
- assuming every A2A implementation has the same discovery path or streaming transport;
- accepting a caller identity supplied in task content;
- using task completion as evidence of exactly-once side effects;
- forwarding remote artifacts or callback URLs without validation;
- trusting remote Agent output as instructions or authorization;
- retaining raw task payloads indefinitely;
- advertising framework support without testing the pinned version and transport;
- calling a remote Agent where a local deterministic workflow would be clearer.
Implementation Checklist
- [ ] Pin protocol, binding, SDK, and Agent Card schema revisions.
- [ ] Verify remote endpoint provenance before fetching or trusting a Card.
- [ ] Authenticate both directions where the deployment requires it.
- [ ] Authorize each skill, tenant, object, and side effect at the owning service.
- [ ] Persist idempotency, lifecycle transitions, and uncertainty states.
- [ ] Bound messages, artifacts, callbacks, streaming, retries, and telemetry.
- [ ] Treat remote messages, artifacts, and Card content as untrusted.
- [ ] Test restart, duplicate delivery, cancellation, expiry, cross-tenant, and SSRF cases.
- [ ] Define retention and deletion for tasks, artifacts, indexes, caches, and exports.
- [ ] Roll out behind explicit scope, budget, and rollback controls.
Conclusion
A2A can make independent Agent services interoperable, but it is not an autonomous trust fabric. Pin the version you run, treat discovery as untrusted metadata, make task state durable, and enforce identity, object access, and side effects outside the model. The safest adoption starts with one narrow, read-oriented delegated task and expands only after lifecycle, authorization, and failure evidence are sound.