Auditoría de A2A 1.0: tres bindings, cards firmadas, extensión rezagada
A2A llegó a 1.0 en marzo. Lo interesante no es el número de versión: es que casi toda forma JSON que un agente manda por A2A cambió, y la extensión de pagos del ecosistema no siguió.
Clonamos a2aproject/A2A el 15-ago-2026, en HEAD 1eb4aa0 (14-ago-2026), y leímos la especificación, el a2a.proto, los documentos de governance y las capas de compatibilidad dentro de los SDKs oficiales. No los posts de lanzamiento. El repo es Apache-2.0, fue creado el 25-mar-2025 y hoy reporta 25,352 stars, 2,570 forks y 231 issues abiertas.
Escribimos sobre A2A cuando todavía era una propuesta v0.x en el post del estándar de interoperabilidad. Este es el seguimiento que un operador realmente necesita: qué se rompió, cuál es el nuevo primitivo de confianza y qué partes del stack no se pusieron al día.
Qué es 1.0, en fechas
Los tags cuentan la historia más rápido que cualquier anuncio. v0.3.0 se publicó el 30-jul-2025. Después ocho meses de nada, y v1.0.0 el 12-mar-2026. Un solo patch desde entonces: v1.0.1, publicado el 28-may-2026, con entrada de changelog fechada 26-may-2026 y tres bug fixes — uno de ellos, el PR #1753, para "prefer application/a2a+json in HTTP binding".
El texto de anuncio en el repo nombra al Technical Steering Committee: AWS, Cisco, Google, IBM Research, Microsoft, Salesforce, SAP y ServiceNow. El comunicado de la Linux Foundation del 9-abr-2026 ubica la adopción en más de 150 organizaciones, contra más de 50 en abril de 2025, con Microsoft integrando A2A en Azure AI Foundry y Copilot Studio y AWS soportándolo vía Amazon Bedrock AgentCore Runtime. Cuenta cinco SDKs listos para producción — Python, JavaScript, Java, Go y .NET — mientras que el roadmap del repo, actualizado por última vez el 10-mar-2026, dice que el proyecto hospeda seis, agregando Rust.
Esa es la forma de un protocolo que ya superó la pregunta de adopción y pasó a la pregunta de mantenimiento. Que es exactamente donde duelen los breaking changes.
Un data model, tres bindings y una regla
El movimiento estructural de 1.0 es que a2a.proto dejó de ser el archivo de gRPC y pasó a ser la fuente normativa de verdad para todos los bindings. JSON-RPC, gRPC y HTTP+JSON quedan definidos como mapeos del mismo modelo, serializados según la especificación ProtoJSON adoptada en el ADR-001.
La sección 5.1 lo vuelve requisito, no aspiración. Cuando un agente expone más de un binding, todos MUST ofrecer funcionalidad idéntica, resultados semánticamente equivalentes, mapeo consistente de errores y los mismos esquemas de autenticación. Hay una tabla canónica de mapeo de métodos: SendMessage es POST /message:send en REST, GetTask es GET /tasks/{id}, CancelTask es POST /tasks/{id}:cancel. Once operaciones, tres columnas, cero features específicos de un binding.
Los bindings custom están permitidos y se identifican por URI, no por nombre. Un Agent Card puede anunciar "protocolBinding": "https://example.com/bindings/websocket/v1", y un cambio breaking en ese binding MUST recibir una URI nueva. La misma disciplina que usa la capa de extensiones, aplicada a los transportes.
El wire cambió casi en todas partes
El documento de migración de v0.3 a 1.0 en el repo tiene 975 líneas. La versión corta: si escribiste un parser contra 0.3, ahora está mal.
TextPart, FilePart y DataPart ya no existen. Hay un solo mensaje Part con un oneof, y el tipo de contenido se determina por qué miembro está presente:
message Part {
oneof content {
string text = 1;
bytes raw = 2; // base64 en JSON
string url = 3;
google.protobuf.Value data = 4;
}
google.protobuf.Struct metadata = 5;
string filename = 6;
string media_type = 7; // reemplaza mimeType, para todo tipo de part
}
El discriminador kind desapareció de los parts y de los eventos de stream. Un status update ya no es {"kind": "status-update", ...}; es {"statusUpdate": {...}}, y un artifact update es {"artifactUpdate": {..., "index": 0}}. El booleano final se eliminó por completo: el estado terminal lo señala el mecanismo de cierre de stream propio de cada binding.
Todos los valores de enum se reescribieron a SCREAMING_SNAKE_CASE con prefijo de tipo, según ProtoJSON. "completed" pasó a "TASK_STATE_COMPLETED", "input-required" a "TASK_STATE_INPUT_REQUIRED", "user" a "ROLE_USER". Los timestamps ahora son explícitamente ISO 8601 UTC con precisión de milisegundos.
Los errores se mudaron a google.rpc.Status. Las respuestas HTTP+JSON que usaban problem details de RFC 9457 con application/problem+json ahora devuelven application/json con un google.rpc.ErrorInfo en details, con reason en UPPER_SNAKE_CASE y domain: "a2a-protocol.org". La taxonomía de errores de A2A son nueve entradas, desde TaskNotFoundError (-32001, gRPC NOT_FOUND, HTTP 404) hasta ExtensionSupportRequiredError (-32008) y VersionNotSupportedError (-32009).
El Agent Card también se reestructuró. url, preferredTransport, additionalInterfaces y protocolVersion desaparecieron del nivel superior, consolidados en un array supportedInterfaces[] donde cada entrada lleva su propio url, protocolBinding y protocolVersion. El listado de tasks llegó como operación nueva, ListTasks, con paginación por cursor. Los nombres compuestos de recurso como tasks/{id}/pushNotificationConfigs/{configId} se partieron en campos planos, y el prefijo /v1 se sacó de las rutas REST.
La negociación de versión es un header
Toda la historia de compatibilidad descansa en un solo service parameter. Los clientes MUST mandar A2A-Version en cada request, en forma Major.Minor; los números de patch quedan explícitamente fuera de la negociación. Los agentes MUST servir la semántica pedida o devolver VersionNotSupportedError. Los clientes MAY pasarlo como query parameter en vez de header, lo cual importa en entornos que maltratan headers custom.
A2A-Version vacío MUST interpretarse como 0.3, no como la versión más nueva. Un cliente que olvida el header no falla ruidosamente; recibe en silencio semántica 0.3 de cualquier agente que todavía la hable.
Como protocolVersion ahora vive por interfaz, un mismo agente puede anunciar 0.3 y 1.0 en paralelo, en la misma URL o en distintas, y dejar que el cliente elija. El Agent Card evolucionó de forma retrocompatible precisamente para que ese anuncio dual sea posible mientras el protocolo de interacción sí se rompía.
Los SDKs implementan esto como una capa de compatibilidad explícita, no como tolerancia best-effort. En el SDK de Python, src/a2a/utils/constants.py define PROTOCOL_VERSION_1_0, PROTOCOL_VERSION_0_3 y PROTOCOL_VERSION_CURRENT = PROTOCOL_VERSION_1_0, y todo un paquete a2a/compat/v0_3/ contiene conversiones de modelo más transportes JSON-RPC y REST que estampan el header 0.3 en las llamadas salientes. El SDK de JavaScript lo refleja: src/compat/v0_3/ despacha según el header — los requests cuya versión no está en [0.3, 1.0) se rutean hacia adelante — y su handler de Agent Card sirve un ETag por versión con Vary: A2A-Version.
Ese último detalle es el que hay que copiar. Contenido negociado por versión detrás de un CDN necesita el header Vary o vas a cachear una card 0.3 y servírsela a un cliente 1.0.
Los números de versión de los SDKs, por su parte, no siguen al protocolo. Al momento de la auditoría los últimos releases eran a2a-python v1.1.2 (22-jul-2026), a2a-js v1.0.1 (28-jul-2026), a2a-java v1.2.0.Final (7-ago-2026), a2a-go v2.4.0 (28-jul-2026) y a2a-dotnet v1.0.0-preview2 (9-abr-2026). Lee las constantes, no el tag.
Las Agent Cards firmadas son el primitivo nuevo de verdad
Un Agent Card es una declaración de capacidades autopublicada que se busca en /.well-known/agent-card.json. Hasta 1.0 no había nada que verificar. Ahora las cards MAY llevar un array signatures[] de firmas JWS según RFC 7515, sobre un payload canonicalizado con el JSON Canonicalization Scheme del RFC 8785.
Las reglas de canonicalización son más sutiles que "ordenar las claves". Antes de aplicar RFC 8785, el JSON tiene que respetar la presencia de campos de protobuf: los campos optional nunca seteados MUST omitirse, los optional seteados explícitamente a un default MUST incluirse, los required siempre están presentes incluso en su default, y los campos repeated vacíos se descartan salvo que sean required. La spec recorre un ejemplo donde "extensions": [] se omite mientras "streaming": false sobrevive, produciendo exactamente:
{"capabilities":{"pushNotifications":false,"streaming":false},"description":"","name":"Example Agent","skills":[]}
El campo signatures se excluye del payload para evitar la dependencia circular. Cada firma es un AgentCardSignature con un header protected en base64url, una signature en base64url y un objeto header no protegido opcional. El header protegido MUST llevar alg, typ (SHOULD ser "JOSE") y kid, y MAY llevar jku apuntando a un JWKS. Se permiten múltiples firmas explícitamente para soportar rotación de llaves.
La verificación son seis pasos: extraer, resolver la llave vía kid/jku o un key store confiable, quitar defaults, excluir signatures, canonicalizar, verificar. Los clientes SHOULD verificar al menos una firma antes de confiar en una card.
Fíjate en los verbos modales. Firmar es opcional para quien publica y verificar es un SHOULD para el cliente, lo que significa que en la práctica la mayoría de las cards en producción seguirán sin firmar por un tiempo. Pero el mecanismo ya está especificado con suficiente detalle como para exigirlo por política, y compone con el trabajo de identidad a nivel HTTP que cubrimos en Web Bot Auth: una card firmada dice quién publicó la declaración de capacidades, una firma RFC 9421 dice quién está haciendo este request ahora.
Tenant se volvió un campo del protocolo
La multi-tenancy dejó de ser una convención de despliegue. AgentInterface ahora tiene un string opaco tenant, y cuando está seteado los clientes MUST repetirlo en el campo tenant de cada request. Las anotaciones HTTP del proto lo hornean en las rutas — /{tenant}/message:send, /{tenant}/tasks/{id}, /{tenant}/extendedAgentCard.
El protocolo deliberadamente no define el formato ni la semántica del valor. Es una clave de ruteo, y el servidor decide qué significa. Para cualquiera que corra muchos agentes detrás de un gateway — que es la forma de toda plataforma de agentes, la nuestra incluida — esto elimina la última razón para inventar un esquema propietario de rutas.
Un marco de governance sin nada adentro todavía
Las extensiones se declaran en el Agent Card bajo capabilities.extensions[], cada una un AgentExtension con uri, description, un booleano required y un struct params opcional. Los clientes optan por request con un header A2A-Extensions separado por comas, y repiten las URIs que están usando en el array extensions[] del propio mensaje, con el payload bajo metadata indexado por la misma URI.
Las reglas de negociación son estrictas en el sentido correcto. Las extensiones SHOULD versionarse en la URI, un cambio breaking MUST recibir una URI nueva, y un agente que no soporta la versión pedida MUST NOT caer en silencio a una anterior — ignora la extensión, o devuelve ExtensionSupportRequiredError si la card la marcó como required.
Alrededor de eso, la governance de la era 1.0 define dos niveles para los artefactos hospedados bajo la organización a2aproject: repos oficiales llamados ext-{name} y cpb-{name} con URIs bajo https://a2a-protocol.org/extensions/ y /bindings/, y repos experimentales con prefijo experimental-. La promoción requiere un maintainer que patrocine, una implementación de referencia con calidad de producción, evidencia de adopción y un voto del TSC.
Acá viene la parte empírica. El 15-ago-2026 la organización a2aproject lista 17 repositorios públicos. La cantidad llamada ext-* o cpb-* es cero. Hay exactamente dos artefactos en incubación: experimental-cpb-slimrpc, un binding SLIMRPC, y experimental-ext-oid4vp-auth, una extensión de autorización in-task con OID4VP. Cinco meses después de 1.0, el namespace oficial de extensiones está vacío.
Lo cual es un problema para los pagos
La extensión que más nos importa ni siquiera está en esa organización. La extensión x402 de A2A vive en google-agentic-commerce/a2a-x402 — 551 stars — y se identifica con URLs de GitHub: https://github.com/google-a2a/a2a-x402/v0.1 para v0.1, una URL blob/main/spec/v0.2 para v0.2. Ninguna está bajo el namespace oficial, lo cual es legítimo — cualquiera puede publicar una extensión de forma independiente — pero significa que la capa de pagos queda fuera del sistema de niveles construido para darle a las extensiones un camino de promoción.
Más concreto: ambas versiones de la spec están escritas contra el formato de wire de 0.3. Las leímos. Los ejemplos de v0.2 todavía muestran "kind": "task", "kind": "message", "parts": [{"kind": "text", ...}] y "state": "input-required" — cada una de esas formas fue eliminada o renombrada en 1.0. El commit más reciente de la rama por defecto es 125db55, del 24-may-2026. El directorio de schemes se expandió de costado en cambio, con variantes Lightning, Spark y UMA de exact.
Así que un agente que actualiza al formato 1.0 y quiere cobrar por una task no puede copiar los ejemplos de la extensión tal cual. La máquina de estados sigue funcionando — la recorrimos en el deep dive de la extensión x402 de A2A — porque va montada sobre claves de metadata y estados de task, y ambos sobreviven la migración. Lo que se rompe es cada literal del documento: los discriminadores, la ortografía de los enums, la forma de los parts. Es un arreglo mecánico que nadie mergeó.
La sección de IANA es una plantilla, no un registro
La sección 14 de la especificación se lee como trabajo terminado: plantillas de registro para el media type application/a2a+json, para los headers A2A-Version y A2A-Extensions, y para el sufijo de URI .well-known/agent-card.json marcado como "Status: Permanent".
Lo verificamos. El 15-ago-2026, el registro de media types de aplicación de IANA no contiene ninguna entrada que coincida con a2a, y el registro de well-known URIs no contiene ninguna que coincida con agent. Las plantillas además siguen llevando una dirección de contacto placeholder en example.org, y el ejemplo del registro de A2A-Version muestra 0.3. Mientras tanto v1.0.1 gastó un bug fix en preferir ese media type no registrado en el binding HTTP.
No es un escándalo; los registros provisionales toman tiempo y la spec dice que las plantillas están "intended for submission". Es un recordatorio de que "está en la spec" y "está registrado" son estados distintos, y que la infraestructura que valida content types de forma estricta debería tratar application/a2a+json como vendor-specific por ahora.
Qué significa para LLM4Agents
A2A es la capa por encima de nosotros y deberíamos ser indiferentes a qué versión hable un agente — pero la indiferencia hay que ingenierizarla.
Primero, la postura del gateway. LLM4Agents es un plano de inferencia y settlement compatible con OpenAI; A2A es cómo el agente que nos llama habla con sus pares. Cuando nuestros clientes exponen sus propios agentes por A2A, lo que nos toca es la costura de pago: una task pasa a estado de pago requerido, el cliente firma una autorización EIP-3009, el merchant agent liquida. Cada uno de esos literales tiene ahora dos ortografías según la versión negociada, y la spec de la extensión solo documenta la vieja. Todo lo que publiquemos como plantilla — samples, docs, un merchant agent de referencia — tiene que declarar a qué A2A-Version apunta y emitir las formas de enum correspondientes.
Segundo, las Agent Cards firmadas son el primitivo de identidad que deberíamos estar leyendo, no solo produciendo. Nuestro modelo de facturación ya sabe autenticar un request; lo que no sabe es si el agente contraparte en una cadena delegada es quien su card dice ser. Un JWS sobre una card canonicalizada con JCS, con la llave resuelta vía jku, es un chequeo barato y verificable offline que compone con la identidad de wallet que ya atamos a una cuenta.
Tercero, el campo tenant es directamente útil. Muchos agentes detrás de un endpoint, con una clave de ruteo a nivel de protocolo que el cliente está obligado a repetir, es exactamente nuestra topología. También es una dimensión limpia de auditoría: el tenant que generó una task es el tenant que facturamos.
Cuarto, y lo más importante estratégicamente: el namespace oficial de extensiones vacío es una apertura. El camino de governance existe, los criterios de promoción están publicados, y los pagos son la capacidad ausente más obvia en un protocolo cuyo propio comunicado habla de escenarios empresariales y regulados. La distancia entre "la extensión x402 existe" y "existe una extensión oficial de pagos de A2A" es hoy un maintainer que patrocine y una implementación de referencia.
Cómo mantenerse en la frontera
Pasos concretos, en el orden en que los tomaríamos.
1. Fijar y detectar la versión en el edge. Cualquier superficie nuestra que dé a A2A lee A2A-Version, trata el valor vacío como 0.3 exactamente como exige la spec, y se niega a adivinar. Las cards servidas por CDN llevan Vary: A2A-Version. Es una semana de trabajo y evita el modo de falla donde un cliente 1.0 recibe en silencio semántica 0.3.
2. Verificar cards firmadas antes de confiar en las capacidades declaradas. Implementar canonicalización JCS con las reglas de presencia de protobuf — la parte sutil es la eliminación de valores por defecto, no el orden de las claves — y verificar al menos una firma JWS. Cachear el JWKS de jku, honrar la rotación de llaves vía múltiples firmas, y loguear las cards sin firmar en vez de rechazarlas mientras el ecosistema se pone al día.
3. Publicar un mapeo de pago x402 nativo de 1.0. Tomar la máquina de estados de la extensión, reexpresar cada ejemplo en formas 1.0 — TASK_STATE_INPUT_REQUIRED, parts discriminados por miembro, envoltorios statusUpdate — y contribuirlo upstream en vez de guardarlo interno. El lado x402 de ese mapeo es estable; auditamos el framework de extensiones en la auditoría de la capa de extensiones de x402 y el formato del envelope no se movió.
4. Usar el campo tenant en vez de inventar uno. Donde ruteamos múltiples agentes por un endpoint, adoptar la clave de ruteo opaca del protocolo y volverla la clave de join entre identidad de task e identidad de facturación.
5. Seguir los dos repos experimentales. experimental-ext-oid4vp-auth es autorización in-task con presentaciones verificables, que es el mismo problema de delegación que tienen los pagos en stablecoins. Lo que gradúe primero fija el patrón de cómo se ve una extensión oficial — incluido cómo se esperará que se vean las extensiones de pago.
A2A 1.0 hizo el trabajo poco glamoroso: un data model, tres bindings sujetos a equivalencia funcional, un formato de firma para las declaraciones de capacidades, una clave de ruteo, y un header que deja convivir lo viejo con lo nuevo. Lo que no hizo es darle a los agentes una forma de cobrarse entre ellos. Eso sigue siendo una extensión, esa extensión sigue en el formato de wire viejo, y el namespace donde viviría una oficial está vacío.
Settlement que no depende de qué versión hables
Inferencia pay-per-call en stablecoins, sobre un gateway compatible con OpenAI. Tu agente negocia A2A con sus pares; nosotros manejamos el dinero.
Registrar un agente