The A2A protocol hit v1.0.0 on March 12, 2026. I wrote a quickstart last week — two agents talking to each other locally, under 15 minutes. This is the follow-up: what actually changed in v1.0.0, and what it means for agents going beyond localhost.
The short version: v1.0.0 isn't just a version bump. It landed four things that matter for production deployments: signed agent identity, proper OAuth flows for headless agents, multi-tenancy, and paginated task listing. None of these appeared in v0.x.
The SDK (a2a-sdk on PyPI) is still at v0.3.25 stable. There's a v1.0.0a0 alpha if you want to live on the edge. That gap is actually useful context — it tells you what the spec can do that the SDK hasn't exposed yet, and where to plan ahead.
What v1.0.0 Added
The full changelog is on the a2aproject/A2A GitHub repo. Here's what matters practically.
1. Signed Agent Cards (JWS)
The Agent Card is how one agent discovers another: its name, capabilities, supported input/output types, and endpoint URL. In v0.x, Agent Cards were plain JSON — no authentication of the card itself. Any server could claim to be any agent.
v1.0.0 adds JWS (JSON Web Signature) to Agent Cards. An agent can now cryptographically sign its own card, and a caller can verify the signature before trusting the card.
Why this matters: In a multi-agent system with agents from different teams or vendors, you can't assume every agent card is legitimate. JWS verification gives you a trust root at the identity layer — before any task is delegated.
Implementation sketch (conceptual — SDK alpha required for full support):
import json
from jose import jws, jwk
def sign_agent_card(card: dict, private_key_pem: str) -> str:
"""Sign an Agent Card and return the JWS compact serialization."""
key = jwk.construct(private_key_pem, algorithm="RS256")
payload = json.dumps(card).encode()
return jws.sign(payload, key, algorithm="RS256")
def verify_agent_card(token: str, public_key_pem: str) -> dict:
"""Verify a signed Agent Card and return the card dict."""
key = jwk.construct(public_key_pem, algorithm="RS256")
payload = jws.verify(token, key, algorithms=["RS256"])
return json.loads(payload)
In practice, you'd embed the JWS token in the /.well-known/agent.json response, and clients verify before registering the agent in their registry. The SDK will expose this cleanly once v1.0.0 stable ships — for now, the pyJWT or python-jose approach works against the spec.
2. OAuth 2.0: Device Code Flow + PKCE
v0.x had basic OAuth 2.0 support. v1.0.0 modernized it in two specific ways that matter for agent deployments:
Device Code Flow (urn:ietf:params:oauth:grant-type:device_code): For agents that run headless — no browser, no interactive login, no user present. Instead of redirecting to a login page (which headless agents can't handle), the agent polls a device authorization endpoint while the user approves on a separate device.
import asyncio
import httpx
import time
async def device_code_auth(auth_server: str, client_id: str, scope: str) -> str:
"""Complete the OAuth 2.0 Device Code flow. Returns access token."""
async with httpx.AsyncClient() as client:
# Step 1: Request device code
resp = await client.post(
f"{auth_server}/device/code",
data={"client_id": client_id, "scope": scope}
)
resp.raise_for_status()
device_auth = resp.json()
print(f"Go to: {device_auth['verification_uri']}")
print(f"Enter code: {device_auth['user_code']}")
# Step 2: Poll for token
interval = device_auth.get("interval", 5)
expires_in = device_auth["expires_in"]
deadline = time.time() + expires_in
while time.time() < deadline:
await asyncio.sleep(interval)
token_resp = await client.post(
f"{auth_server}/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_auth["device_code"],
"client_id": client_id,
}
)
token_data = token_resp.json()
if token_data.get("access_token"):
return token_data["access_token"]
if token_data.get("error") == "authorization_pending":
continue
break
raise RuntimeError("Device authorization timed out or was denied")
PKCE (Proof Key for Code Exchange): For agents that do use the authorization code flow but can't safely store a client secret — typical in agents deployed as native apps or CLI tools. PKCE replaces the client secret with a one-time verifier/challenge pair generated per-request.
import hashlib
import base64
import secrets
def generate_pkce_pair() -> tuple[str, str]:
"""Return (code_verifier, code_challenge) for PKCE flow."""
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
return code_verifier, code_challenge
# In authorization request:
verifier, challenge = generate_pkce_pair()
auth_url = (
f"{auth_server}/authorize"
f"?response_type=code"
f"&client_id={client_id}"
f"&redirect_uri={redirect_uri}"
f"&code_challenge={challenge}"
f"&code_challenge_method=S256"
)
# In token exchange:
# Include code_verifier (not the challenge) in the token request
These aren't new OAuth flows — they're RFC 8628 and RFC 7636. What's new is that A2A v1.0.0 specifies them as the expected flows for agent-to-agent authentication, rather than leaving it open.
3. Multi-Tenancy
v0.x A2A assumed one principal per agent endpoint. v1.0.0 adds multi-tenancy: a single A2A server can host multiple isolated tenant contexts, with credentials and tasks namespaced per tenant.
This matters if you're building an A2A agent that multiple organizations or teams use. Without multi-tenancy, you'd run a separate server process per tenant. With v1.0.0, one server handles all of them.
What changes in practice:
# v0.x: one server, one context
from a2a.server.apps import A2AStarletteApplication
server = A2AStarletteApplication(agent=my_agent)
# serve with uvicorn: uvicorn app:server --host 0.0.0.0 --port 8000
# v1.0.0: multi-tenant pattern (SDK support varies — check current docs)
# Tenant context is passed in the task request headers or URL path
# /tenants/{tenant_id}/tasks (path-based) or X-Tenant-ID header
# The agent implementation receives tenant context and scopes its work accordingly
async def handle_task(task: Task, tenant_id: str) -> TaskResult:
db = get_tenant_db(tenant_id)
config = get_tenant_config(tenant_id)
# ... process with tenant-scoped resources
The exact SDK surface for multi-tenancy will stabilize in SDK v1.0.0 stable. For now, if you need it, implement it at the HTTP layer using path prefixes or request headers.
4. tasks/list with Filtering and Pagination
v0.x tasks/list returned everything: all tasks for an agent, flat list, no pagination. Fine for local development; unusable at scale.
v1.0.0 adds:
-
Cursor-based pagination —
nextCursorfield for stable pagination across pages - Filtering — by status, date range, input type
-
State filtering — query only
submitted,working,completed, orfailedtasks
import httpx
async def list_recent_failed_tasks(
agent_url: str,
access_token: str,
cursor: str | None = None
) -> dict:
"""Fetch failed tasks with pagination."""
params = {
"status": "failed",
"pageSize": 50,
}
if cursor:
params["cursor"] = cursor
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{agent_url}/tasks",
params=params,
headers={"Authorization": f"Bearer {access_token}"}
)
resp.raise_for_status()
return resp.json()
# Returns: {"tasks": [...], "nextCursor": "...", "totalCount": 123}
# Paginate through all failed tasks:
async def get_all_failed_tasks(agent_url: str, token: str) -> list:
all_tasks = []
cursor = None
while True:
page = await list_recent_failed_tasks(agent_url, token, cursor)
all_tasks.extend(page["tasks"])
cursor = page.get("nextCursor")
if not cursor:
break
return all_tasks
5. Error Handling: google.rpc.Status
v0.x had minimal standardized error responses. v1.0.0 adopts google.rpc.Status for error payloads, which gives you structured errors with a machine-readable code, a human message, and optional detail objects.
# A v1.0.0 error response looks like:
{
"error": {
"code": 9, # FAILED_PRECONDITION
"message": "Task input type 'audio/wav' not supported by this agent",
"details": [
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{
"field": "input.type",
"description": "Supported types: text/plain, application/json"
}
]
}
]
}
}
# Error handling in your client:
async def delegate_task(agent_url: str, task: dict) -> dict:
async with httpx.AsyncClient() as client:
resp = await client.post(f"{agent_url}/tasks", json=task)
if not resp.is_success:
error = resp.json().get("error", {})
code = error.get("code")
message = error.get("message", "Unknown error")
# google.rpc codes: 0=OK, 3=INVALID_ARGUMENT, 5=NOT_FOUND, 9=FAILED_PRECONDITION, ...
raise A2AError(code=code, message=message, details=error.get("details", []))
return resp.json()
Code 9 (FAILED_PRECONDITION) is worth calling out — it's for "task rejected because a precondition wasn't met," which covers the common case of sending the wrong input type to a specialist agent.
The Spec-vs-SDK Gap: What It Actually Means
The A2A protocol spec is at v1.0.0. The a2a-sdk package on PyPI is at v0.3.25 stable, with a v1.0.0a0 alpha.
This isn't a warning sign — it's normal for a protocol-first project. The spec stabilizes first. The SDK catches up. The gap exists because the spec team and the SDK team (even if it's the same people) have different release velocity.
What it means practically:
You can implement v1.0.0 features today by calling the HTTP API directly. The spec is the contract. The SDK is a convenience wrapper. Everything in this article works without the SDK.
The a2a-sdk v0.3.25 still works fine for the quickstart-level patterns — agent discovery, task delegation, message exchange. Those APIs haven't changed.
v1.0.0a0 alpha is available if you want the full SDK surface for signed cards and multi-tenancy. Production caution applies (alpha = API might shift before stable).
SDK v1.0.0 stable will ship — and when it does, the migration from v0.3.25 will be straightforward. The protocol hasn't broken backward compatibility.
Where to Go From Here
The quickstart gives you two agents talking on localhost. This article gives you the production primitives.
The logical next step is putting them together: a signed, OAuth-authenticated, multi-tenant A2A setup that you'd actually deploy. That's more infrastructure than a 15-minute tutorial allows — but the building blocks are here.
If you're at the "I want to actually deploy this" stage, the pieces in order:
- Sign your Agent Cards (JWS) so callers can verify identity
- Use Device Code flow for headless agents that need to authenticate
- Add PKCE to authorization code flows for non-server agents
- Implement multi-tenancy at the HTTP layer if you're serving multiple orgs
- Use
tasks/listpagination for any monitoring or debugging tooling
The protocol is production-ready. The SDK is catching up. That gap is a window.
The AI Dev Toolkit includes prompts for designing agent systems, reviewing A2A implementations, and generating structured task schemas — the kind of thing that's tedious to write from scratch every time you start a new agent project.
Top comments (0)