2026-07-15
A2A Protocol 1.0 Migration: A 32-Case Cutover Drill
A tag-pinned A2A v0.3-to-v1 migration drill uses a dual-interface Agent Card and 32 fail-closed cases to protect serialization, rollback, and task visibility.
Tag-to-tag migration drill
A2A 1.0 is not a package update. The stable release moves version identity into each advertised interface, makes a2a.proto the normative source, renames operations, reshapes message parts, adds tenant-aware requests, and changes the JSON form of enums. A client can compile cleanly and still send the wrong wire contract.
The safest migration unit is therefore one request, not one repository. Advertise a v1.0 interface beside the v0.3 fallback, select exactly one from the Agent Card, bind the request to that interface, and retain a rollback owner until authorization and result parity survive a canary.
We checked that idea against the official v0.3.0 and v1.0.0 tags, then ran a 32-case admission fixture. Two complete packets were accepted: one v1 path and one deliberate v0.3 fallback. Thirty single-control mutations stopped before a protocol call.
a81082266a44df6ba7397c5c0aac4b29e8e8974a4b0bd8a32ee1c5136b5f31a3.The old Card cannot explain the new wire
In v0.3, the Agent Card carried one top-level protocolVersion, a preferred transport, and additional interfaces. A v1 card instead exposes supportedInterfaces[]. Every interface has its own URL, protocol binding, and protocol version. That is more than a field shuffle: it allows one agent to describe a v1 JSON+HTTP endpoint and a v0.3 JSON-RPC endpoint at the same time.
This is the useful compatibility seam. A caller should read the Card, choose a supported interface, and carry that choice as an immutable tuple through discovery, authentication, serialization, routing, telemetry, and retry. Re-running selection halfway through a request is how a v1 payload reaches a legacy endpoint or a legacy enum slips into a v1 body.
That tuple is the migration unit.
The A2A specification permits multiple bindings. Our two-interface packet is a migration policy, not a claim that every implementation must expose this exact pair.
The tag diff moves the center of gravity
The official migration document says v1 elevates a2a.proto from a gRPC-specific implementation file to the universal, normative source of truth. That choice pulls JSON+HTTP, gRPC, and JSON-RPC toward the same operation and message model. It also makes a tag-to-tag proto comparison more valuable than a blog summary or an SDK changelog.
At the exact tags used here, the v0.3 service defines TaskSubscription and GetAgentCard. The v1 service defines SubscribeToTask and GetExtendedAgentCard, and adds ListTasks. Push-notification configuration methods also change names and request shapes. A broad search-and-replace cannot decide which changes affect storage, authorization, pagination, or streaming closure.
| Migration surface | v0.3 shape | v1.0 shape | Cutover consequence |
|---|---|---|---|
| Interface identity | Top-level version plus preferred and additional transports | supportedInterfaces[], with version and binding per interface | Choose and pin one tuple per request |
| Task discovery | GetTask; no protocol-level list operation | GetTask plus ListTasks and cursor pagination | Scope list and lookup to the authenticated caller |
| Subscription | TaskSubscription / tasks/resubscribe | SubscribeToTask; stream completion belongs to the binding | Do not preserve a synthetic final flag |
| Message content | Legacy JSON part variants | One Part with one selected content branch | Reject zero-branch and multi-branch bodies |
| JSON enums | Values such as user and working | ROLE_USER and TASK_STATE_WORKING | Golden fixtures must assert serialized values |
The quietest break is inside Part
The migration guide describes v0.3 JSON as separate TextPart, FilePart, and DataPart representations. V1 presents one unified Part with a single selected branch: text, raw bytes, URL, or structured data. The old proto already had a Part oneof, which makes the change easy to underestimate. What changed is the normative model and its binding-level representation.
A tolerant decoder is risky here. If it accepts both a legacy discriminator and two new content branches, the application must invent precedence. Different SDKs may invent different answers. Our admission rule takes the boring route: one interface version, one serializer, one content branch. A malformed part never reaches business logic.
Mixed shapes are a protocol error, not a compatibility feature.
File handling deserves a separate regression set. V1 replaces mimeType with mediaType and generalizes file metadata while distinguishing raw content from a URL. That affects size limits, fetch policy, content sniffing, and redaction. A compiler cannot prove those operational rules survived the rename.
A dual-interface Card buys rollback, not ambiguity
Keeping v0.3 alive during a canary is useful only if the fallback is explicit. The caller should not try v1, catch every error, and silently replay through v0.3. That turns authorization failures and invalid payloads into cross-protocol retries. It can also duplicate a non-idempotent message.
Our local packet requires the requested and selected versions to agree. The selected binding must match the advertised interface. For v1 JSON+HTTP, the packet records an A2A-Version value and rejects a reintroduced /v1 path prefix because the stable HTTP mapping removed that protocol-wide prefix. For the legacy fallback, the packet records a null v1 header and an old operation name. These are deliberately separate states.
Agent Card signatures add another boundary. V1 specifies JSON canonicalization and JWS machinery; “the HTTPS request succeeded” is not equivalent to “the advertised interface is trusted.” During migration, cache the verified Card with its signature result and expiry. Do not merge fields from two fetches into a synthetic Card that was never signed.
Testing the version seam
The two passing fixtures are almost dull. One binds a v1 JSON+HTTP interface to SendMessage, a v1 Part, ProtoJSON enum values, a matching tenant and owner, the required scope, and a recorded rollback owner. The other deliberately binds the legacy interface to its old operation and payload vocabulary.
The mutations expose where a real rollout drifts. We altered the pinned source hash, removed one advertised interface, mismatched the requested and selected versions, changed the binding, dropped the version header, restored a legacy operation on the v1 route, inserted two Part branches, reused lowercase enum values, crossed a tenant boundary, changed the task owner, removed a scope, disabled fallback, and erased the rollback owner. Every mutation stopped at the expected gate.
This distinction matters. A conformance suite asks whether an implementation follows the protocol. A cutover suite asks whether one organization can move traffic without mixing versions, widening visibility, or losing rollback. They overlap, but they are not interchangeable.
Task lookup becomes an authorization decision
V1 adds tenant fields to request messages and a protocol-level ListTasks. The specification also says task visibility must be scoped to the caller. That turns what looks like a storage query into an authorization decision. Returning “not found” for a foreign task may be correct even when the row exists.
The dangerous migration bug is to add the tenant field to the DTO while leaving the repository query unchanged. The API looks v1-shaped, yet GetTask, ListTasks, cancellation, subscription, and push-configuration lookups still search a global task table. Pagination makes the leak less obvious: a cursor can carry state from one visibility set into another unless tenant and subject participate in its integrity check.
Visibility is part of the task key.
Apply the same predicate before opening a stream or resolving a push endpoint. Otherwise the read path is isolated while the notification path leaks. The local fixture blocks a tenant mismatch and an owner mismatch independently because either one is enough to reject the request.
Cut over one thin vertical slice
Start with one operation that has a bounded side effect and good observability. GetTask is attractive, but it can hide authorization mistakes behind happy-path fixtures. A stronger slice includes interface discovery, one signed Card, one authenticated subject, a tenant-scoped lookup, the v1 serialized response, and a controlled v0.3 comparison.
Record the selected interface tuple at ingress. Emit separate metrics for version, binding, operation, tenant class, adapter decision, and terminal result—without logging tokens or message contents. Compare semantic results, not raw JSON, because expected enum and shape changes make byte equality useless. A rollback should disable v1 selection for new requests; it should not rewrite in-flight task state.
Only after that slice survives should ListTasks, streaming subscription, cancellation, push notification configuration, and extended Agent Cards follow. Each adds a different failure surface. Grouping them into one flag creates a rollback too large to reason about.
Failure modes this drill cannot cover
The retained evidence is a static comparison of official Git tags plus deterministic local JavaScript. It did not install an A2A SDK, start an agent, fetch a live Agent Card, verify a real JWS key, contact an OAuth issuer, create a tenant, write a task, open a stream, send a push notification, exercise a network canary, or alter production traffic.
The 32-case fixture proves that the admission rules encoded here fail closed under the listed mutations. It does not prove interoperability, performance, SDK correctness, or protocol conformance. Those require a real implementation, official conformance tests, and a separately authorized canary.
Pinned records for this migration
- A2A Protocol v0.3.0 tag — legacy gRPC-specific proto and operation surface, resolved here to commit
210f03d426e2f2fa92000e14ef0de3b7ba15aee5. - A2A Protocol v1.0.0 tag — stable normative proto and migration document, resolved here to commit
173695755607e884aa9acf8ce4feed90e32727a1. - Official “What’s New in A2A Protocol v1.0” — operation, Agent Card, Part, enum, tenant, OAuth, signature, and versioning changes.
- A2A Protocol specification — binding behavior, security, task lifecycle, error, and visibility requirements.
- Local dual-interface fixture — 13 source assertions, 32 decisions, zero mismatches, and 30 rejected mutations. SHA-256:
a81082266a44df6ba7397c5c0aac4b29e8e8974a4b0bd8a32ee1c5136b5f31a3.