A2A 1.0 audit: three bindings, signed cards, one lagging extension
A2A reached 1.0 in March. The interesting part is not the version number: it is that almost every JSON shape an agent sends over A2A changed, and the ecosystem's payment extension did not follow.
We cloned a2aproject/A2A on 2026-08-15, at HEAD 1eb4aa0 (2026-08-14), and read the specification, the a2a.proto, the governance documents and the compatibility layers inside the official SDKs. Not the launch posts. The repository is Apache-2.0, was created on 2025-03-25, and today reports 25,352 stars, 2,570 forks and 231 open issues.
We wrote about A2A when it was still a v0.x proposal in the interop standard post. This is the follow-up an operator actually needs: what broke, what the new trust primitive is, and which parts of the stack have not caught up.
What 1.0 is, in dates
The release tags tell the story faster than any announcement. v0.3.0 was published on 2025-07-30. Then eight months of nothing, and v1.0.0 on 2026-03-12. A single patch release since: v1.0.1, published 2026-05-28, whose changelog entry is dated 2026-05-26 and contains three bug fixes — one of them, PR #1753, to "prefer application/a2a+json in HTTP binding".
The announcement text in the repo names the Technical Steering Committee: AWS, Cisco, Google, IBM Research, Microsoft, Salesforce, SAP and ServiceNow. The Linux Foundation press release of 2026-04-09 puts adoption at more than 150 organizations, up from more than 50 in April 2025, with Microsoft integrating A2A into Azure AI Foundry and Copilot Studio and AWS supporting it through Amazon Bedrock AgentCore Runtime. It counts five production-ready SDKs — Python, JavaScript, Java, Go and .NET — while the repo roadmap, last updated 2026-03-10, says the project hosts six, adding Rust.
That is the shape of a protocol that has cleared the adoption question and moved on to the maintenance question. Which is exactly where breaking changes hurt.
One data model, three bindings, and a rule
The structural move in 1.0 is that a2a.proto stopped being the gRPC file and became the normative source of truth for every binding. JSON-RPC, gRPC and HTTP+JSON are now defined as mappings of the same model, serialized per the ProtoJSON specification adopted in ADR-001.
Section 5.1 makes that a requirement, not an aspiration. When an agent exposes more than one binding, all of them MUST provide identical functionality, semantically equivalent results, consistent error mapping and the same authentication schemes. There is a canonical method mapping table: SendMessage is POST /message:send in REST, GetTask is GET /tasks/{id}, CancelTask is POST /tasks/{id}:cancel. Eleven operations, three columns, no binding-specific features.
Custom bindings are allowed and are identified by URI rather than by name. An Agent Card can advertise "protocolBinding": "https://example.com/bindings/websocket/v1", and a breaking change to that binding MUST get a new URI. The same discipline the extension layer uses, applied to transports.
The wire changed almost everywhere
The v0.3 to 1.0 migration document in the repo is 975 lines. The short version: if you wrote a parser against 0.3, it is wrong now.
TextPart, FilePart and DataPart no longer exist. There is one Part message with a oneof, and content type is determined by which member is present:
message Part {
oneof content {
string text = 1;
bytes raw = 2; // base64 in JSON
string url = 3;
google.protobuf.Value data = 4;
}
google.protobuf.Struct metadata = 5;
string filename = 6;
string media_type = 7; // replaces mimeType, for every part type
}
The kind discriminator is gone from parts and from stream events. A status update is no longer {"kind": "status-update", ...}; it is {"statusUpdate": {...}}, and an artifact update is {"artifactUpdate": {..., "index": 0}}. The final boolean was removed entirely: terminal state is signalled by the binding's own stream closure.
Every enum value was rewritten to SCREAMING_SNAKE_CASE with a type prefix, per ProtoJSON. "completed" became "TASK_STATE_COMPLETED", "input-required" became "TASK_STATE_INPUT_REQUIRED", "user" became "ROLE_USER". Timestamps are now explicitly ISO 8601 UTC with millisecond precision.
Errors moved to google.rpc.Status. HTTP+JSON responses that used RFC 9457 problem details with application/problem+json now return application/json with a google.rpc.ErrorInfo in details, carrying reason in UPPER_SNAKE_CASE and domain: "a2a-protocol.org". The A2A error taxonomy is nine entries, from TaskNotFoundError (-32001, gRPC NOT_FOUND, HTTP 404) through ExtensionSupportRequiredError (-32008) and VersionNotSupportedError (-32009).
The Agent Card was restructured too. url, preferredTransport, additionalInterfaces and protocolVersion all disappeared from the top level, consolidated into a supportedInterfaces[] array where each entry carries its own url, protocolBinding and protocolVersion. Task listing arrived as a new operation, ListTasks, with cursor-based pagination. Compound resource names like tasks/{id}/pushNotificationConfigs/{configId} were split into plain fields, and the /v1 prefix was dropped from REST paths.
Version negotiation is one header
The compatibility story rests on a single service parameter. Clients MUST send A2A-Version on every request, in Major.Minor form; patch numbers are explicitly excluded from negotiation. Agents MUST serve the requested semantics or return VersionNotSupportedError. Clients MAY pass it as a query parameter instead of a header, which matters for environments that mangle custom headers.
A2A-Version MUST be interpreted as 0.3, not as the newest version. A client that forgets the header does not fail loudly; it silently gets 0.3 semantics from any agent that still speaks it.
Because protocolVersion now lives per interface, one agent can advertise 0.3 and 1.0 side by side, on the same URL or different ones, and let the client pick. The Agent Card evolved in a backward-compatible way precisely so this dual advertisement is possible while the interaction protocol itself broke.
The SDKs implement this as an explicit compatibility layer rather than as best-effort tolerance. In the Python SDK, src/a2a/utils/constants.py defines PROTOCOL_VERSION_1_0, PROTOCOL_VERSION_0_3 and PROTOCOL_VERSION_CURRENT = PROTOCOL_VERSION_1_0, and an entire a2a/compat/v0_3/ package holds model conversions plus JSON-RPC and REST transports that stamp the 0.3 header on outbound calls. The JavaScript SDK mirrors it: src/compat/v0_3/ dispatches on the header — requests whose version is not in [0.3, 1.0) are routed onward — and its Agent Card handler serves a version-specific ETag with Vary: A2A-Version.
That last detail is the one to copy. Version-negotiated content behind a CDN needs the Vary header or you will cache a 0.3 card and serve it to a 1.0 client.
SDK version numbers, meanwhile, do not track the protocol. At audit time the latest releases were a2a-python v1.1.2 (2026-07-22), a2a-js v1.0.1 (2026-07-28), a2a-java v1.2.0.Final (2026-08-07), a2a-go v2.4.0 (2026-07-28) and a2a-dotnet v1.0.0-preview2 (2026-04-09). Read the constants, not the tag.
Signed Agent Cards are the real new primitive
An Agent Card is a self-published capability claim fetched from /.well-known/agent-card.json. Until 1.0 there was nothing to verify. Now cards MAY carry a signatures[] array of JWS signatures per RFC 7515, over a payload canonicalized with the JSON Canonicalization Scheme of RFC 8785.
The canonicalization rules are more subtle than "sort the keys". Before applying RFC 8785, the JSON has to respect protobuf field presence: optional fields never set MUST be omitted, optional fields explicitly set to a default MUST be included, required fields are always present even at their default, and repeated fields that are empty are dropped unless required. The spec walks a worked example where "extensions": [] is omitted while "streaming": false survives, producing exactly:
{"capabilities":{"pushNotifications":false,"streaming":false},"description":"","name":"Example Agent","skills":[]}
The signatures field is excluded from the payload to avoid the circular dependency. Each signature is an AgentCardSignature with a base64url protected header, a base64url signature, and an optional unprotected header object. The protected header MUST carry alg, typ (SHOULD be "JOSE") and kid, and MAY carry jku pointing at a JWKS. Multiple signatures are allowed explicitly to support key rotation.
Verification is six steps: extract, resolve the key via kid/jku or a trusted key store, strip defaults, exclude signatures, canonicalize, verify. Clients SHOULD verify at least one signature before trusting a card.
Note the modal verbs. Signing is optional for the publisher and verification is a SHOULD for the client, which means in practice most cards in the wild will remain unsigned for a while. But the mechanism is now specified well enough to be enforced by policy, and it composes with the HTTP-layer identity work we covered in Web Bot Auth: a signed card says who published the capability claim, an RFC 9421 signature says who is making this request right now.
Tenant became a protocol field
Multi-tenancy stopped being a deployment convention. AgentInterface now has an opaque tenant string, and when it is set clients MUST echo it in the tenant field of every request. The proto's HTTP annotations bake it into the paths — /{tenant}/message:send, /{tenant}/tasks/{id}, /{tenant}/extendedAgentCard.
The protocol deliberately does not define the format or semantics of the value. It is a routing key, and the server decides what it means. For anyone running many agents behind one gateway — which is the shape of every agent platform, ours included — this removes the last reason to invent a proprietary path scheme.
A governance framework with nothing in it yet
Extensions are declared in the Agent Card under capabilities.extensions[], each an AgentExtension with uri, description, a required boolean and an optional params struct. Clients opt in per request with a comma-separated A2A-Extensions header, and echo the URIs they are using in the message's own extensions[] array, with payload under metadata keyed by the same URI.
The negotiation rules are strict in the right way. Extensions SHOULD version themselves in the URI, a breaking change MUST get a new URI, and an agent that does not support a requested version MUST NOT silently fall back to an older one — it ignores the extension, or returns ExtensionSupportRequiredError if the card marked it required.
Around that, 1.0-era governance defines two tiers for artifacts hosted under the a2aproject organization: official repos named ext-{name} and cpb-{name} with URIs under https://a2a-protocol.org/extensions/ and /bindings/, and experimental repos prefixed experimental-. Promotion requires a maintainer sponsor, a production-quality reference implementation, evidence of adoption and a TSC vote.
Here is the empirical part. On 2026-08-15 the a2aproject organization lists 17 public repositories. The number named ext-* or cpb-* is zero. There are exactly two incubating artifacts: experimental-cpb-slimrpc, a SLIMRPC binding, and experimental-ext-oid4vp-auth, an OID4VP in-task authorization extension. Five months after 1.0, the official extension namespace is empty.
Which is a problem for payments
The extension that matters most to us is not in that organization at all. The A2A x402 extension lives in google-agentic-commerce/a2a-x402 — 551 stars — and identifies itself with GitHub URLs: https://github.com/google-a2a/a2a-x402/v0.1 for v0.1, a blob/main/spec/v0.2 URL for v0.2. Neither is under the official namespace, which is legitimate — anyone may publish an extension independently — but it means the payment layer sits outside the tier system that was built to give extensions a promotion path.
More concretely: both spec versions are written against the 0.3 wire format. We read them. The v0.2 examples still show "kind": "task", "kind": "message", "parts": [{"kind": "text", ...}] and "state": "input-required" — every one of those shapes was removed or renamed in 1.0. The default branch's most recent commit is 125db55, dated 2026-05-24. The schemes directory has been expanding sideways instead, with Lightning, Spark and UMA variants of exact.
So an agent that upgrades to the 1.0 wire format and wants to charge for a task cannot copy the extension's examples verbatim. The state machine still works — we walked through it in the A2A x402 extension deep dive — because it rides on metadata keys and task states, both of which survive the migration. What breaks is every literal in the document: the discriminators, the enum spellings, the part shapes. That is a mechanical fix nobody has merged.
The IANA section is a template, not a registration
Section 14 of the specification reads like a finished job: registration templates for the application/a2a+json media type, for the A2A-Version and A2A-Extensions headers, and for the .well-known/agent-card.json URI suffix marked "Status: Permanent".
We checked. On 2026-08-15, IANA's application media types registry contains no entry matching a2a, and the well-known URIs registry contains none matching agent. The templates also still carry a placeholder contact address at example.org, and the A2A-Version registration example shows 0.3. Meanwhile v1.0.1 spent a bug fix on preferring that unregistered media type in the HTTP binding.
This is not a scandal; provisional registrations take time and the spec says the templates are "intended for submission". It is a reminder that "in the spec" and "registered" are different states, and infrastructure that validates content types strictly should treat application/a2a+json as vendor-specific for now.
What it means for LLM4Agents
A2A is the layer above us and we should be indifferent to which version an agent speaks — but indifference has to be engineered.
First, the gateway posture. LLM4Agents is an OpenAI-compatible inference and settlement plane; A2A is how the agent that calls us talks to its peers. When our customers expose their own agents over A2A, the thing that touches us is the payment seam: a task moves to a payment-required state, the client signs an EIP-3009 authorization, the merchant agent settles. Every one of those literals now has two spellings depending on the negotiated version, and the extension spec only documents the old one. Anything we ship as a template — samples, docs, a reference merchant agent — has to state which A2A-Version it targets and emit the matching enum forms.
Second, signed Agent Cards are the identity primitive we should be reading, not just producing. Our billing model already knows how to authenticate a request; what it does not know is whether the counterparty agent in a delegated chain is who its card claims. A JWS over a JCS-canonicalized card, with the key resolved via jku, is a cheap, offline-verifiable check that composes with the wallet identity we already bind to an account.
Third, the tenant field is directly useful. Many agents behind one endpoint, with a protocol-level routing key the client is required to echo, is exactly our topology. It is also a clean audit dimension: the tenant that generated a task is the tenant we bill.
Fourth, and most important strategically: the empty official extension namespace is an opening. The governance path exists, the promotion criteria are published, and payments are the most obvious missing capability in a protocol whose own press release talks about enterprise and regulated scenarios. The gap between "the x402 extension exists" and "an official A2A payments extension exists" is currently a maintainer sponsor and a reference implementation.
Staying on the frontier
Concrete steps, in the order we would take them.
1. Pin and detect the version at the edge. Any A2A-facing surface we ship reads A2A-Version, treats the empty value as 0.3 exactly as the spec requires, and refuses to guess. Cards served over a CDN get Vary: A2A-Version. This is a week of work and it prevents the failure mode where a 1.0 client silently receives 0.3 semantics.
2. Verify signed cards before trusting capability claims. Implement JCS canonicalization with the protobuf presence rules — the subtle part is default-value removal, not key ordering — and verify at least one JWS signature. Cache the JWKS from jku, honor key rotation via multiple signatures, and log unsigned cards rather than rejecting them while the ecosystem catches up.
3. Publish a 1.0-native x402 payment mapping. Take the extension's state machine, re-express every example in 1.0 shapes — TASK_STATE_INPUT_REQUIRED, member-discriminated parts, statusUpdate wrappers — and contribute it upstream rather than keeping it internal. The x402 side of that mapping is stable; we audited the extension framework in the x402 extensions layer audit and the envelope format has not moved.
4. Use the tenant field instead of inventing one. Where we route multiple agents through one endpoint, adopt the protocol's opaque routing key and make it the join key between task identity and billing identity.
5. Track the two experimental repos. experimental-ext-oid4vp-auth is in-task authorization with verifiable presentations, which is the same delegation problem stablecoin payments have. Whatever graduates first sets the pattern for how an official extension looks — including how payment extensions will be expected to look.
A2A 1.0 did the unglamorous work: one data model, three bindings held to functional equivalence, a signature format for capability claims, a routing key, and a header that lets old and new coexist. What it did not do is give agents a way to charge each other. That is still an extension, that extension is still on the old wire format, and the namespace where an official one would live is empty.
Settlement that does not care which version you speak
Pay-per-call inference in stablecoins, over an OpenAI-compatible gateway. Your agent negotiates A2A with its peers; we handle the money.
Register an agent