{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreihf6ovkag6nmotpyffk2tjxwbmemwpefofrsrbulplbscrpfw3bse",
"uri": "at://did:plc:eza3tsy3stk7a3w2ajez3sca/app.bsky.feed.post/3miykz7ns63g2"
},
"description": "A practical guide to Google A2A agents - covering setup, API key vs OAuth authentication, deployment to Cloud Run, and registration with Gemini Enterprise.",
"path": "/from-a2a-agent-to-gemini-enterprise-a-practical-deployment-guide/",
"publishedAt": "2026-04-08T15:06:18.000Z",
"site": "https://alphasec.io",
"tags": [
"Agent2Agent (A2A)",
"Linux Foundation",
"ADK",
"MCP",
"Gemini Enterprise",
"github.com/alphasecio/google-a2a",
"open protocol",
"Railway",
"Cloud Run",
"DigitalOcean",
"repository",
"JWTs",
"GitHub repo",
"https://your-service.up.railway.app/.well-known/agent.json"
],
"textContent": "The Agent2Agent (A2A) protocol is an open standard for communication and collaboration between AI agents. Originally developed by Google, and now managed by the Linux Foundation, A2A aims to untangle the web of diverse agentic frameworks by different vendors, making agents speak one definitive common language for seamless interoperability. Where ADK is about building agents, and MCP is about interacting with tools, A2A dictates how agents talk to each other, to platforms like Gemini Enterprise, and even to humans. If you're building enterprise agents, A2A is definitely worth understanding.\n\nThis post walks through a working demo: two A2A servers (one using API key auth, one using OAuth 2.0), a Streamlit web client, CLI test clients, and how to register everything in Gemini Enterprise. All code is at github.com/alphasecio/google-a2a.\n\n### What is A2A?\n\nA2A is an open protocol that standardises how AI agents communicate. An A2A server publishes an **Agent Card** at `./well-known/agent.json` - a JSON document describing the agent's name, description, skills, supported input/output modes, capabilities, and authentication requirements. Clients discover the agent via this card, and communicate with it using JSON-RPC over HTTP. The protocol sits a layer above transport and does not care whether you're running on Railway, Cloud Run, DigitalOcean, or somewhere else. It does care about authentication though, which is a large focus of this walkthrough.\n\nHow A2A works with MCP (source: a2a-protocol.org)\n\n### The Demo Repo\n\nThe repository is structured around two independent A2A `hello` servers, and a couple of test CLI and web clients.\n\n\n ├── server/\n │ ├── hello_apikey/ # API key auth\n │ └── hello_oauth/ # OAuth 2.0 auth\n └── client/\n ├── streamlit_app.py # Web GUI\n ├── hello_apikey_client.py # CLI test client\n └── hello_oauth_client.py # CLI test client\n\n\nBoth servers implement the same two skills: a public `hello` skill (no auth required) and a `roll_dice` skill (auth required). The difference lies in their authentication mode - one uses API tokens, other uses OAuth 2.0. Each server exposes:\n\n * A public agent card listing only the `hello` skill\n * An authenticated extended card (at `/agent/authenticatedExtendedCard`) listing both skills, returned only when a valid token is supplied\n\n\n\n### API Key vs OAuth 2.0\n\nThe `hello_apikey` server validates a static `AGENT_API_TOKEN` environment variable against the `Authorization: Bearer` header on every request. It's simple, works everywhere, with zero additional dependencies, but the token is a typical bearer token - it never expires, and there's no easy way to rotate it automatically.\n\nThe `hello_oauth` server validates JWTs using OIDC discovery. On startup, it fetches `/.well-known/openid-configuration` from the configured OAuth provider to get the JWKS URI, then validates incoming tokens against the provider's public keys. No shared secret - tokens are short-lived and cryptographically signed.\n\nNot all OAuth providers are set up the same though. During my testing, I ran into one Microsoft Entra-specific quirk: the `OAUTH_AUDIENCE` is the `Application (client) ID`, not a separate URL as it would be with Auth0. The server handles this transparently, but take note of this when setting up environment variables.\n\nAnother interesting observation is that the auth header doesn't reach the A2A SDK's `RequestContext` - it gets stripped during request processing. The OAuth server works around this with a lightweight ASGI middleware that captures the raw `Authorization` header into a context variable before the SDK processes the request:\n\n\n class AuthHeaderMiddleware:\n def __init__(self, app):\n self.app = app\n\n async def __call__(self, scope, receive, send):\n if scope[\"type\"] in (\"http\", \"websocket\"):\n headers = dict(scope.get(\"headers\", []))\n auth_header = headers.get(b\"authorization\", b\"\").decode(\"latin1\")\n auth_token_var.set(auth_header)\n await self.app(scope, receive, send)\n\nThe API key server avoids this by passing the token in the A2A message `metadata` field instead, which does survive SDK processing.\n\n### Deploying the A2A Server (API Key) to Railway\n\nRailway is a modern platform that hosts your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it. If you don't already have an account, sign up using GitHub, and click `Authorize Railway App` when redirected. New users get a one-time $5 trial credit (30 days), after which the Hobby plan costs $5/month. To deploy the `hello_apikey` server:\n\n * Fork or clone the GitHub repo\n * Create a new Railway service, set the root directory to `server/hello_apikey`\n * Railway detects the `Dockerfile` automatically and builds it\n\n\n\nAdd the following environment variables in Railway's service settings:\n\n * `AGENT_BASE_URL`: Your Railway public URL (e.g. `https://hello-apikey.up.railway.app`)\n * `AGENT_API_TOKEN`: A strong random secret (`openssl rand -hex 32`)\n\n\n\nOnce deployed, verify the agent card (https://your-service.up.railway.app/.well-known/agent.json) is live - it should look something like this:\n\n\n {\n \"capabilities\": {\n \"streaming\": false\n },\n \"defaultInputModes\": [\n \"text/plain\"\n ],\n \"defaultOutputModes\": [\n \"text/plain\"\n ],\n \"description\": \"Simple agent with a public hello skill and an authenticated dice-rolling skill.\",\n \"iconUrl\": \"https://cdn.jsdelivr.net/gh/googlefonts/noto-emoji@main/png/128/emoji_u1f44b.png\",\n \"name\": \"Hello A2A\",\n \"preferredTransport\": \"JSONRPC\",\n \"protocolVersion\": \"0.3.0\",\n \"skills\": [\n {\n \"description\": \"Returns a piratey greeting. No authentication required.\",\n \"examples\": [\n \"hi\",\n \"hello\",\n \"hi, i'm bob\"\n ],\n \"id\": \"hello\",\n \"name\": \"Hello\",\n \"tags\": [\n \"hello\",\n \"greeting\"\n ]\n }\n ],\n \"supportsAuthenticatedExtendedCard\": true,\n \"url\": \"https://your-service-url.up.railway.app\",\n \"version\": \"1.0.0\"\n }\n\n### Deploying the A2A Server (OAuth) to Cloud Run\n\n`hello_oauth` is a natural fit for Cloud Run—Google's OAuth providers (including Entra via federation) integrate cleanly, and Cloud Run handles scaling to zero.\n\nFrom inside `server/hello_oauth`:\n\n\n gcloud run deploy hello-a2a-oauth \\\n --source . \\\n --region us-central1 \\\n --allow-unauthenticated \\\n --set-env-vars \\\n AGENT_BASE_URL=https://your-service-url.run.app,\\\n OAUTH_ISSUER=https://your-tenant.auth0.com,\\\n OAUTH_AUDIENCE=https://your-agent-url\n\nFor Entra, set `OAUTH_AUDIENCE` to the Application (client) ID of your registered app, and `OAUTH_ISSUER` to `https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0`. If you're using v1.0, the `OAUTH_ISSUER` will be `https://sts.windows.net/YOUR_TENANT_ID/`.\n\nThe `--allow-unauthenticated` flag is intentional—Cloud Run's transport-layer auth is separate from the A2A protocol's application-layer auth. The agent handles its own authentication; no need for another layer for a public-facing A2A endpoint.\n\n### Deploying the Streamlit Client\n\nThe CLI clients `hello_apikey_client.py` and `hello_oauth_client.py` can be run anywhere Python is installed - your development machine, GitHub Codespaces, or elsewhere. The Streamlit client is an interactive app that connects to any A2A-compliant agent, inspects its agent card, and lets you chat with it. It supports three auth modes: none, API token, and OAuth 2.0 (client credentials flow).\n\nDeploy it to Railway or Cloud Run the same way—set the root directory to `client/` and it will find the `Dockerfile`. For Railway, the `PORT` environment variable is injected automatically. For Cloud Run:\n\n\n cd client\n gcloud run deploy a2a-client \\\n --source . \\\n --region us-central1 \\\n --allow-unauthenticated\n\nTo run it locally:\n\n\n cd client\n cp .env.example .env # fill in values\n uv pip install -e .\n streamlit run streamlit_app.py\n\n### Registering the Agents with Gemini Enterprise\n\nGemini Enterprise (GE) is an intranet search, AI assistant, and agentic platform by Google. It includes prebuilt connectors for commonly-used third-party applications like Confluence, Jira, Microsoft SharePoint, and ServiceNow. It can also provide conversational assistance, answer complex questions, and host custom AI agents (including external A2A agents) that apply generative AI contextually.\n\nGemini Enterprise end user application (source: docs.cloud.google.com)\n\nA few things to note before registering the A2A agent:\n**MIME types matter.** The agent card must use valid MIME types for `defaultInputModes` and `defaultOutputModes`. `\"text\"` is rejected—use `\"text/plain\"`. This isn't enforced by the A2A SDK but GE validates it strictly.\n\n**OAuth is required.** Gemini Enterprise (GE) requires OAuth 2.0 for agent authentication - there's no API key option in the registration UI. This means you need the `hello_oauth` server to register with GE. You'll need to provide:\n\n * Client ID\n * Client Secret\n * Auth URL (`https://your-tenant/authorize`)\n * Token URL (`https://your-tenant/oauth/token`)\n * Scopes (can be empty for basic client credentials flow). For Entra specifically, you may need to configure `openid profile email offline_access YOUR_TENANT_ID/.default`.\n\n\n\n**The agent card URL** is your server's base URL. GE fetches `/.well-known/agent.json` from it automatically on registration.\n\nOnce registered, the agent appears in the list of organisational agents for your Gemini Enterprise app users and can be invoked in conversations. You'll be required to authorise the action when invoking the `roll_dice` skill.\n\n### Final Thoughts\n\nThe A2A SDK is still evolving fast—`A2AClient` is already deprecated in favour of `ClientFactory` in recent releases, though the migration path has some rough edges. The demo deliberately uses `A2AClient` for the client code since it's stable and functional, with deprecation warnings suppressed. For production use, the static API key approach is discouraged, and the OAuth approach should be followed instead. As A2A matures and the SDK stabilises, expect the client-side patterns here to simplify—but the server-side architecture and auth model should hold.",
"title": "From A2A Agent to Gemini Enterprise: A Practical Deployment Guide",
"updatedAt": "2026-04-08T15:06:18.848Z"
}