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.

A2A Protocol 1.0 Migration: A 32-Case Cutover Drill cover illustration

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.

Retained result: 13 source assertions and all 32 expected decisions matched. The fixture contains one v1-ready packet, one v0.3-fallback packet, and 30 blocked variants across 19 distinct failure reasons. Fixture SHA-256: 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.

2complete interface paths retained in the cutover fixture
30one-control mutations rejected before any network request
19distinct fail-closed reasons exercised by the local policy

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 surfacev0.3 shapev1.0 shapeCutover consequence
Interface identityTop-level version plus preferred and additional transportssupportedInterfaces[], with version and binding per interfaceChoose and pin one tuple per request
Task discoveryGetTask; no protocol-level list operationGetTask plus ListTasks and cursor paginationScope list and lookup to the authenticated caller
SubscriptionTaskSubscription / tasks/resubscribeSubscribeToTask; stream completion belongs to the bindingDo not preserve a synthetic final flag
Message contentLegacy JSON part variantsOne Part with one selected content branchReject zero-branch and multi-branch bodies
JSON enumsValues such as user and workingROLE_USER and TASK_STATE_WORKINGGolden 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.

Risograph illustration of several nested geometric containers entering a mechanical press and emerging as one four-compartment vessel
The migration is a type-boundary change. Conversion happens once, at the selected interface adapter, rather than opportunistically throughout the application.

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.

Interface tupleCard signature status, canonicalization rule, URL, binding, and protocol version are retained together.
Serialization tupleOperation name, Part representation, role enum, task-state enum, and path are selected from the same adapter.
Caller tupleAuthenticated subject, tenant, required scopes, and visible task owner travel with the request.
Rollout tupleCanary parity, fallback availability, and a named rollback owner decide whether v1 remains enabled.

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.

What the count means: 32 is the size of this deterministic policy fixture, not the number of conformance requirements in A2A v1.0. The official specification remains authoritative. The fixture exists to make our cutover assumptions executable and reviewable.

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.

Risograph illustration of a matching teal task token passing a double-key gate while a coral cross-tenant token is stopped by a barrier
A task identifier is necessary but insufficient. The visible object is the intersection of task ID, tenant, authenticated subject, and granted scope.

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.

Decision: migrate with two advertised interfaces and one selected adapter per request. Remove v0.3 only after the v1 slice proves semantic parity and caller-scoped task visibility under real traffic.

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.