Tres agentes de IA, cada uno construido con el framework de un proveedor distinto y corriendo en el hosting de ese mismo proveedor, respondiendo la misma pregunta al mismo tiempo: un agente ADK en Cloud Run (Google), un agente Strands en Bedrock AgentCore Runtime (AWS) y un agente Agent Framework en Container Apps (Azure).
Un coordinador los llama a todos por el protocolo A2A v1.0 y toma la mediana de sus respuestas. Y, lo más interesante: no hay ni una credencial de larga duración almacenada en el sistema en ejecución — cada llamada se autentica con un token acuñado en el momento exacto en que se necesita.
El proyecto completo está en github.com/xbill9/multicloud-adk-a2a-currency y se puede levantar en una laptop en menos de un minuto. Pero la sorpresa no fue el protocolo: A2A funcionó. La sorpresa fue que casi todas las decisiones que importaban se tomaron antes de que ocurriera una sola llamada A2A.
¿Por qué molestarse? “Solo usa una key”
Tienes un agente en una nube. Alguien te pide que llame a un agente en otra. El reflejo natural es crear una service account key, guardarla en un secret manager y seguir adelante. Eso funciona — y también significa que ahora eres dueño de una credencial para siempre: rotarla, delimitar su alcance, auditar su uso, y eventualmente explicarle a alguien por qué producción contiene una key estática.
Existe otro camino, y lo interesante es que no es más difícil. Solo se decide antes.
La decisión que define todo lo demás
Esta es la asimetría de la que se desprende todo el diseño:
Todos los agentes que quieras llamar pueden consumir un token externo. AWS IAM tiene OIDC identity providers. Entra tiene Federated Identity Credentials. AgentCore acepta un CUSTOM_JWT. Los tres confiarán en un token acuñado en otro lugar, siempre que configures la confianza correctamente.
Pero solo algunos runtimes pueden acuñar uno. Un runtime capaz de producir un workload OIDC token — para una audiencia que tú elijas — puede federarse hacia cualquiera de ellos. Uno que no puede, vuelve a guardar una credencial.
Así que “¿dónde corre mi coordinador?” es en realidad “¿cuántos secretos tendrá este sistema?”
| Host del coordinador | Conexiones que hace | Secretos de larga duración |
|---|---|---|
| Cloud Run | GCP→AWS, GCP→Azure, GCP→GCP | potencialmente cero |
| AgentCore | AWS→Azure, AWS→GCP | al menos uno |
| Foundry | Azure→AWS, Azure→GCP | uno o dos, ambos no probados |
Cloud Run gana porque su metadata server te entrega un ID token para cualquier audiencia que nombres — exactamente lo que las trust policies de las otras dos nubes quieren ver. Que AgentCore pueda hacer lo mismo no está confirmado (el autor no lo probó). O sea: “cero secretos” es una propiedad de esta topología, no una ley universal de los agentes cross-cloud.
Esa elección tiene dos costos que vale la pena decir en voz alta:
Una conexión deja de ser cross-cloud. El coordinador corre en Cloud Run, así que la pata GCP es Google llamando a Google. Se cruzan dos fronteras de vendor, no tres. Eso pertenece a los resultados, no a una nota al pie.
No puedes correrlo localmente. Una credencial de usuario no puede acuñar un ID token de audiencia arbitraria — gcloud auth print-identity-token --audiences=... se niega rotundamente y te dice que requiere una service account. No existe versión de laptop para este camino. Una vez que eliges federación, el único lugar donde el sistema funciona es donde está desplegado.
Tres conexiones, tres mecanismos, una sola costura
Las conexiones no se parecen entre sí:
- GCP → GCP — un ID token cuya audiencia es la URL del servicio destino, más
roles/run.invoker. - GCP → AWS — acuñas ese token, se lo entregas a STS
AssumeRoleWithWebIdentity, obtienes credenciales temporales y firmas la petición con SigV4. - GCP → Azure — acuñas ese token y lo presentas a Entra como client assertion contra una Federated Identity Credential, obteniendo un access token.
Dos bearer tokens y una firma de petición. Formas completamente distintas. El movimiento que hizo manejable todo lo demás fue poner los tres detrás de una sola interfaz: httpx.Auth. Para httpx, un header bearer y una firma sobre el body de la petición son el mismo tipo de objeto. Los tres SDK de vendor aceptan un httpx.AsyncClient. Así, la credencial se adjunta una sola vez y todo lo que pasa por ese client la lleva.
auth = credentials_for(peer, endpoint) # un httpx.Auth, o None
client = load_client(stack, endpoint, auth=auth)
Construye esa costura antes de tu segunda nube, no después de la tercera. Si dejas que una conexión funcione con código inline prometiendo generalizar después, terminas con tres estilos de manejo de errores y tres lugares donde se cachea un token.
Vale la pena notarlo: la card de un agente vive en
/.well-known/agent-card.json, detrás de la misma autorización que el agente mismo. Si adjuntas tu credencial a la petición en vez del client, el discovery falla con 403 mientras la llamada real habría funcionado: obtienes un error de protocolo que no apunta a la autenticación. Adjuntar al client lo hace imposible por construcción.
Cinco trampas que parecen configuración correcta
Ninguna de estas es un typo. Cada una es algo que puedes hacer mal mientras estás siendo cuidadoso.
La audiencia no es autorización. El caller elige la audiencia. Así que una trust policy que solo verifica la audiencia demuestra que alguien en ese IdP acuñó un token — no que tu identidad lo hizo. Fija también el subject, y fíjalo al ID numérico inmutable en vez del email, porque los emails pueden liberarse y re-vincularse a otra persona.
AWS y Azure invierten el mismo paso. AWS federates con accounts.google.com de forma nativa — si creas un IAM OIDC provider explícito para eso, lo rompes con InvalidIdentityToken. Entra, en cambio, requiere que crees la credencial explícitamente. La misma tarea conceptual, requisitos opuestos, y ninguno de los dos errores te dice en qué regla estás.
Las condition keys de IAM no contienen lo que sus nombres dicen. accounts.google.com:oaud es el aud del token. accounts.google.com:aud es su azp, que es un número. Si pones un string de audiencia en :aud, escribiste una condición que nunca puede matchear — y la denegación no lo mencionará.
Pide el token completo. El metadata mint de GCP toma format=full. Sin eso, Google recorta claims — incluyendo email — y cualquier trust condition que lea ese claim deja de matchear silenciosamente.
Dos códigos de error valen más que un día de logs. De STS, InvalidIdentityToken significa que el token no validó en absoluto: problema de configuración del provider. AccessDenied significa que validó perfectamente y tus condiciones no matchearon: problema de política. Tardes diferentes.
Todo esto lleva al hábito que el autor llevaría a cualquier proyecto así: loguea la respuesta cruda del provider en cada frontera de auth. En un sistema de agentes, un error vuelve como resultado de herramienta, y un modelo en el medio parafraseará alegremente AccessDenied: condition accounts.google.com:sub did not match como “hubo un problema con las credenciales.” Un mensaje levantado no es un observable.
Cómo ejecutarlo
Empieza local. Tres agentes en loopback, sin cuenta en la nube, alrededor de un minuto:
git clone https://github.com/xbill9/multicloud-adk-a2a-currency
cd multicloud-adk-a2a-currency
uv pip install --system "a2a-sdk[http-server]" google-adk \
agent-framework-a2a agent-framework-core \
pydantic httpx uvicorn pytest pytest-asyncio
uv pip install --system -e .
Levanta los tres agentes y hazles una pregunta:
./infra/run_mesh.sh start # :10001 :10002 :10003
python3 -m coordinator.cli 100 USD EUR JPY
Tres stacks de agentes de vendors distintos respondiendo juntos:
participants: gcp, aws, azure
100 USD = 92 EUR @ 0.92 [3/3 clouds, agreed]
gcp 92 (164ms)
aws 92 (25ms)
azure 92 (12ms)
La demo es la corrida más interesante, porque muestra qué pasa cuando un participante se equivoca:
./infra/demo.sh
Cuatro actos: tres nubes respondiendo, la matriz de interoperabilidad 3×3, una nube que se cae, y una nube que miente. Los últimos dos son el punto — cualquiera puede mostrar tres ticks verdes.
Desplegar de verdad es un script por nube y un comando para conectarlas:
./infra/deploy_aws.sh deploy # AgentCore Runtime + federated role
./infra/deploy_azure.sh deploy # Container App
./infra/deploy_azure.sh fic # Entra app registration + federated credential
./infra/deploy_azure.sh auth # hacer que el ingress realmente lo exija
./infra/deploy_gcp.sh deploy # ADK service + coordinator job
./infra/deploy_gcp.sh wire # plegar las patas AWS y Azure
./infra/deploy_gcp.sh run # consenso de tres nubes, desde la nube
Lo que cuesta en latencia
| GCP (in-cloud) | AWS | Azure | elapsed | |
|---|---|---|---|---|
| rango | 836–948ms | 1027–1109ms | 468–512ms | 1711–1854ms |
El elapsed aterriza aproximadamente un segundo por encima de la conexión individual más lenta, y muy por debajo de la suma de las tres. Las conexiones se emiten en paralelo, así que la suma nunca fue el modelo correcto — pero tampoco la conexión más lenta por sí sola. Ese segundo extra es el costo fijo del coordinador: arranque del contenedor, tres fetches de agent-card, tres acuñados de credenciales.
Vale la pena notarlo: una versión anterior de esta afirmación citaba solo la conexión más lenta y estaba equivocada por 85% en la corrida más rápida. Ese error solo se hizo visible cuando hubo más de una muestra.
La federación en sí es barata. Los mints e intercambios de token son una porción pequeña de ese segundo fijo. Si la malla se siente lenta, es un cold start o el modelo — no el trabajo de identidad.
Lo que esto no demuestra
Un deployment, una cuenta, un par de regiones, una persona, unos días. Estas son pruebas de existencia: una cosa funcionó, en una configuración. No son mediciones de una población.
Es keyless en operación, no en bootstrap. Crear trust policies, app registrations y federated credentials usó credenciales de operador ordinarias — como siempre ocurre con el provisioning.
Y esa afirmación necesitaba verificación, que es la parte honesta. Las tres conexiones A2A siempre fueron keyless — pero la app de Azure jalaba su imagen de contenedor usando la admin password del registry, almacenada como secreto en su propia configuración. No en ningún camino agente-a-agente, y aun así suficiente para hacer “no hay secretos almacenados” falso tal como estaba escrito. Container Apps soporta pull por managed identity, así que el fix fue un role grant y borrar el secreto. Una auditoría de los tres deployments ahora no muestra ninguna credencial almacenada.
El punto general y aburrido: el image pull es parte de tu sistema desplegado. Una afirmación sobre secretos tiene que cubrirlo todo, no solo la parte interesante.
La expiración y renovación de tokens está implementada y probada contra un reloj congelado, pero ningún token ha expirado en producción — cada corrida es un job que vive unos segundos.
Si vas a empezar uno de estos
Decide dónde corre el coordinador antes que cualquier otra cosa; eso fija la cantidad de secretos de todo el sistema. Construye la costura de credenciales antes de la segunda nube. Adjunta la auth al client, no a la petición, para que el discovery quede cubierto. Loguea las palabras del provider en cada frontera, porque pasarás más tiempo leyendo fallas de auth que escribiendo código de auth. Acota tus controles a un componente, porque un sistema construido para sobrevivir fallas te ocultará felizmente una.
Y reconstruyelo desde cero una vez, antes de afirmar que es reproducible.
Repo: github.com/xbill9/multicloud-adk-a2a-currency — los tres agentes, el coordinador, la matriz de interoperabilidad, los scripts de deploy y los write-ups de hallazgos en docs/.
¿Te interesa la orquestación de agentes de IA en producción? Sigue explorando estos temas en el blog de DojoFullStack, donde cubrimos arquitectura de agentes, automatización y desarrollo de software para equipos que construyen en serio.