Agentic AI Foundation Logo
A2A v1.0: a builder's guide, part 2 - migration, security, and production

A2A v1.0: a builder's guide, part 2 - migration, security, and production

Rohit GhumareSeptember 1, 2026

Part 1 covered the core A2A v1.0 flow: discovery, tasks, server and client behavior, and the ways clients can follow progress. You can read part 1 here. This second part picks up with the breaking wire changes in v1.0, then moves through migration from 0.3, bindings, security, durability, authorization, and conformance.

The wire changed underneath you

v1.0's other breaking change is quieter and reaches every parser you wrote. The kind discriminator is gone. Polymorphic objects now identify themselves by which JSON member is present, which is how Protocol Buffers oneof works, and the whole type system moved to proto with ProtoJSON as the canonical serialization. That is also why every enum you handled as "working" is now "TASK_STATE_WORKING".

Fig. 5 · the same event, before and after v1.0

Pick a payload version and the parser reading it. Three of the four combinations are what a mixed fleet actually looks like during a migration.

[Fig. 5 is an interactive figure on the site; a static screenshot is included in the .docx version. Open it at https://rohitghumare.com/blog/a2a-protocol/#wd]

Shapes from Appendix A.2.1 of the spec; compatibility behavior from the Python SDK's v0.3 to v1.0 migration guide.

If you learned A2A from a 2025 write-up, here is the whole delta in one place, so you can tell which of your notes still hold.

What you read in 2025What it is at v1.0Changed in
/.well-known/agent.json/.well-known/agent-card.json, a registered well-known URI0.3, July 2025
AgentCard.url, one endpointsupportedInterfaces, ordered, one entry per binding1.0
Methods named tasks/send, message/sendSendMessage, SendStreamingMessage, GetTask, ListTasks, CancelTask, SubscribeToTask1.0
States submitted, working, completedTASK_STATE_SUBMITTED and friends, plus REJECTED, AUTH_REQUIRED, UNSPECIFIED1.0
Parts tagged with "kind"Member name is the discriminator: text, raw, url, data1.0
supportsAuthenticatedExtendedCard on the cardcapabilities.extendedAgentCard1.0
SDKs: Python and Go, through one frameworkSix SDK repos in the a2aproject org: Python, JavaScript, Java, Go, C#, Rust, plus an integration testing kitthrough 2025 and 2026

The rest of the Python migration is mechanical but wide: the application wrapper classes are gone in favor of route factories, so you compose A2A routes into your own Starlette or FastAPI app and keep your middleware; ClientFactory became await create_client(...); push_notification_config is singular now; helpers consolidated under a2a.helpers; and send_message yields StreamResponse objects you inspect with HasField('artifact_update') instead of isinstance checks. The SDKs ship v1.0 with a 0.3 compatibility mode: the Python SDK covers all three bindings for both spec versions, and the JavaScript SDK reached 1.0 general availability on July 22, 2026, four months after the spec froze. That four month gap is the number to plan around, because the project now carries six language SDKs and they do not land together.

One call, three bindings, and curl for the impatient

The spec requires the three bindings to be functionally equivalent, and it publishes the mapping so you can move between them without guessing. Same operation, three spellings.

OperationJSON-RPC methodgRPC methodREST endpoint
Send messageSendMessageSendMessagePOST /message:send
Stream messageSendStreamingMessageSendStreamingMessage (server stream)POST /message:stream
Get taskGetTaskGetTaskGET /tasks/{id}
List tasksListTasksListTasksGET /tasks
Cancel taskCancelTaskCancelTaskPOST /tasks/{id}:cancel
ResubscribeSubscribeToTaskSubscribeToTask (server stream)POST /tasks/{id}:subscribe
Create push configCreateTaskPushNotificationConfigCreateTaskPushNotificationConfigPOST /tasks/{id}/pushNotificationConfigs
Extended cardGetExtendedAgentCardGetExtendedAgentCardGET /extendedAgentCard

The REST binding is the one you can debug from a terminal, which makes it the one to bring up first even if you ship gRPC later.

typescript
# 1. read the card
curl -s https://agent.example.com/.well-known/agent-card.json | jq '.supportedInterfaces'

# 2. send a message and block until the task settles
curl -s -X POST https://agent.example.com/a2a/rest/message:send \
 -H 'Content-Type: application/a2a+json' \
 -H 'A2A-Version: 1.0' \
 -H "Authorization: Bearer $TOKEN" \
 -d '{"message":{"role":"ROLE_USER","messageId":"msg-91",
      "parts":[{"text":"Route Mountain View to SFO, avoid tolls"}]}}'

# 3. or stream it, and watch the frames arrive
curl -N -X POST https://agent.example.com/a2a/rest/message:stream \
 -H 'Content-Type: application/a2a+json' -H 'A2A-Version: 1.0' \
 -d '{"message":{"role":"ROLE_USER","messageId":"msg-92","parts":[{"text":"..."}]}}'

# 4. poll one task, or list the ones still running in this conversation
curl -s "https://agent.example.com/a2a/rest/tasks/task-7c2?historyLength=10" -H 'A2A-Version: 1.0'
curl -s "https://agent.example.com/a2a/rest/tasks?contextId=ctx-19&status=TASK_STATE_WORKING&pageSize=50" \
 -H 'A2A-Version: 1.0'

That last call is ListTasks, which v1.0 added and which quietly changes what an A2A deployment can be. Before it, a client that lost its task IDs had lost the work; now there is a filtered, cursor-paginated listing, sorted by last update, capped at 100 per page, scoped by the spec to tasks the caller is authorized to see. It is the difference between a protocol for one exchange and a protocol for an operations console.

An unsigned agent card is a suggestion

Everything above assumes the card you fetched is the card the agent published. Threat modeling work on A2A has been consistent about where that assumption breaks. The Cloud Security Alliance's MAESTRO analysis put spoofed cards and weak server identity at the top of the agent-framework layer. Palo Alto Networks' writeup named the two field cases: shadowing, where a card mimics a trusted one and changes only the endpoint URL, and context poisoning, where the descriptions and example prompts inside a card carry injection payloads into the client agent's prompt. The card is fetched over the network, it goes into a model's context, and it is the only thing telling your client where to send work. Treat it as untrusted input in both roles.

The answer is a signature block on the card. The signatures field arrived in 0.3, and v1.0 pinned down how to produce and check it: JSON Web Signature per RFC 7515, over a payload canonicalized with the JSON Canonicalization Scheme, RFC 8785. The steps are exact, which is what makes them implementable.

Fig. 6 · canonicalize, sign, verify, tamper

Step through what a verifier does to a card before it trusts an endpoint. The digest is computed in your browser with SHA-256 over the canonical string, so the tamper switch really does change it.

[Fig. 6 is an interactive figure on the site; a static screenshot is included in the .docx version. Open it at https://rohitghumare.com/blog/a2a-protocol/#sg]

Canonicalization and signature rules from spec sections 8.4.1 to 8.4.3. The demo applies the key ordering and whitespace rules of RFC 8785 and shows a real SHA-256 of the canonical payload; a production verifier checks a JWS signature over that payload with the key named by kid, fetched from the jku key set.

Both SDKs ship the primitives, so you are wiring, not implementing. In Python, signing is a callable you hand the card, and verification is a callable that fetches keys and raises on failure.

python
from a2a.utils.signing import create_agent_card_signer, create_signature_verifier

sign = create_agent_card_signer(
   signing_key=private_jwk,
   protected_header={'alg': 'ES256', 'typ': 'JOSE', 'kid': 'key-1',
                     'jku': 'https://georoute-agent.example.com/jwks.json'},
)
signed_card = sign(agent_card)      # canonicalizes with JCS, appends to card.signatures

verify = create_signature_verifier(key_provider=jwks_lookup, algorithms=['ES256'])
verify(fetched_card)                # raises NoSignatureError / InvalidSignaturesError

The JavaScript SDK exposes the same three moving parts under different names: canonicalizeAgentCard, verifyAgentCardSignature, and an AgentCardSignatureGenerator hook on the request handler. If you only do one security thing this quarter, make it client-side verification, because an unverified card is a URL a stranger chose for you.

Signing fixes card integrity. It does not fix identity, and the spec is careful about the difference: verify the signature, then decide separately whether the organization behind that key is one you delegate work to. Two more rules from the security section belong in your handler on day one. Do not distinguish "task does not exist" from "you may not see this task", because the difference is an enumeration oracle. And scope every task read to the caller's authorized access boundaries, since possession of a task ID alone should not grant access.

Wiring push notifications, both ends

Streaming is the easy path and the wrong one for anything that outlives a deploy. Webhooks are three objects on the server: a config store, a sender, and the handler that owns both.

python
from a2a.server.tasks import (
   BasePushNotificationSender, InMemoryPushNotificationConfigStore, InMemoryTaskStore,
)

push_config_store = InMemoryPushNotificationConfigStore()

handler = DefaultRequestHandler(
   agent_executor=MyAgentExecutor(),
   task_store=InMemoryTaskStore(),
   agent_card=card,                       # capabilities.push_notifications MUST be true
   extended_agent_card=extended_card,     # optional, served only to authenticated callers
   push_config_store=push_config_store,
   push_sender=BasePushNotificationSender(
       httpx_client=notification_client,
       config_store=push_config_store,
   ),
)

The client registers a webhook against a task, and from then on every frame it would have received on the stream arrives as an HTTP POST instead.

bash
curl -s -X POST https://agent.example.com/a2a/rest/tasks/task-7c2/pushNotificationConfigs \
 -H 'Content-Type: application/a2a+json' -H 'A2A-Version: 1.0' \
 -d '{"url":"https://ops.example.com/a2a/hooks/task-7c2",
      "token":"opaque-per-task-secret",
      "authentication":{"scheme":"Bearer","credentials":"secret-for-this-task"}}'

# or attach the same config to the first message, so it is live from the start
curl -s -X POST https://agent.example.com/a2a/rest/message:send \
 -H 'Content-Type: application/a2a+json' -H 'A2A-Version: 1.0' \
 -d '{"message":{"role":"ROLE_USER","messageId":"msg-93","parts":[{"text":"Generate the Q1 report"}]},
      "configuration":{"taskPushNotificationConfig":{
        "url":"https://ops.example.com/a2a/hooks/q1",
        "authentication":{"scheme":"Bearer","credentials":"secure-client-token"}}}}'

Note on that first call: the spec lists the create-config fields in section 3.1.7 and states that REST bodies are structurally equivalent to the Protocol Buffer definitions, but it publishes no worked REST example for this endpoint, so the flat body above is read from the field list rather than copied from the spec. The second form, the config attached to message:send, is taken verbatim from the spec's own section 6.6 example.

Use a distinct token per config, not one shared secret. Rotate it, verify it in constant time, and keep the handler idempotent: the spec requires agents to attempt delivery at least once and explicitly allows duplicates.

Tasks that survive a restart

The default task store is in memory, which is correct for a sample and wrong for anything a customer touches. A task is a durable object in the protocol's model: clients may resubscribe to it, list it, or ask for it hours later, and all of that fails if your process forgot. Swap the store and keep everything else.

pip install "a2a-sdk[postgresql]" # or [mysql], [sqlite], [sql] for all three
python
from sqlalchemy.ext.asyncio import create_async_engine
from a2a.server.tasks import DatabaseTaskStore

engine = create_async_engine('postgresql+asyncpg://user:pass@localhost/a2a')
task_store = DatabaseTaskStore(engine=engine)

handler = DefaultRequestHandler(
   agent_executor=MyAgentExecutor(),
   task_store=task_store,
   agent_card=card,
)

There is a matching DatabasePushNotificationConfigStore, and the pairing matters: a webhook registration that lives only in memory disappears on the deploy that happens while the task is still running, which is exactly the case webhooks existed to cover.

In-task authorization can interrupt a task

The feature I did not expect to find in a v1.0 protocol is in-task authorization, and it is the clearest sign that A2A is designed for work that takes minutes rather than milliseconds. When an agent hits something it cannot do without a credential or a human approval, it moves the task to TASK_STATE_AUTH_REQUIRED and puts an explanation in the status message. The client can answer, negotiate, refuse, or, if the client is itself an agent serving its own task, move its task to AUTH_REQUIRED and pass the request up. Authorization requests chain the same way delegation does.

Two constraints keep that from becoming a credential leak. Credentials should arrive out of band over a channel the requesting agent controls, because in-band credentials passing through a chain are readable by every agent in the chain. And the state transition itself grants nothing: the spec says an agent must not treat AUTH_REQUIRED, by itself, as authorization for any operation, and a credential obtained during one interruption must not be assumed to cover later messages on the same task. If you have built an approval flow inside an agent framework, this is the same problem with the escalation path written down.

How to know it actually conforms

Three tools exist, and using them is faster than reading your own logs. The A2A Inspector is a web UI that talks to any agent and shows the raw frames next to a validation report, which is where card mistakes surface first. The Integration Testing Kit is the cross-SDK conformance harness the project runs against its own implementations, so it is the closest thing to a conformance suite. And the SDKs ship compatibility samples that run a v1.0 server against both a v1.0 and a hand-rolled v0.3 client in one process, which is the cheapest way to prove your compat flag does what you think.

A short bring-up order that avoids the usual dead ends: serve the card and check it in the Inspector before writing any executor logic; send one non-streaming message and confirm you get a task rather than a bare message; open the stream and assert the first event is a Task; then register a webhook and kill the client mid-task to prove delivery is independent of your process.

What A2A still does not give you

A protocol is as useful as its edges are clear. Four things are outside them today. There is no standard registry: discovery is a well-known path plus "querying curated catalogs", and which catalog is your problem, so at fleet scale you are building or buying an index. There is no semantics for skills: AgentSkill is names, tags, and examples, which means matching a task to an agent is still a model call or your own routing table. Multi-tenancy is an opaque string: tenant is echoed and routed by the server, with no defined format, which is honest but pushes the design onto you. And settlement is a separate protocol: the commerce work Google Cloud and PayPal are doing runs the payment authorization layer beside A2A rather than inside it.

One gap builders complain about is observability. A single request can now cross an A2A boundary into an agent that calls MCP servers through a gateway, and each layer is instrumented separately. The A2A half of that trace is at least legible: tasks have IDs, contexts group them, artifacts are addressable, and the SDK ships OpenTelemetry as an extra. Start emitting spans keyed on taskId and contextId before you have a fleet, not after.

What to build this week

If you want a working agent rather than an opinion about protocols, the path is short. Publish a card at /.well-known/agent-card.json with one interface and honest capabilities, and only advertise features your agent actually supports. Pick a single binding to start: JSON-RPC has the widest client support, HTTP+JSON is the one you can debug with curl, gRPC is worth it when you are inside one cluster. Write the executor task-first and pick one streaming pattern. Set returnImmediately: true on anything slower than a page load, and register a webhook rather than holding a connection open across a deploy. Then run the A2A Inspector against your agent and read the raw frames it shows you, because the first bug is almost always in the card, not the code.

The governance move matters for exactly one practical reason. The version policy, the deprecation path, and the meetings where breaking changes get argued continue under Linux Foundation governance within AAIF, with published technical steering meetings, which is a different kind of dependency than a protocol a single vendor ships on its own schedule. Given that v1.0 already broke every 0.3 parser once, knowing where the next break will be argued is worth as much as the spec itself.

Spec behavior, SDK versions, and dates here come from the A2A specification, the SDK repositories, and the foundation's own announcements, read on August 17, 2026. Protocol details change between minor versions, so check the version your card advertises before relying on any of it.

Share

Author

subscription section bg
Subscribe

Subscribe to the AAIF Briefing

Weekly signal on standards, governance, and the people building the future. No fluff. Just what matters.

About AAIF