This project is a functional proof-of-concept demonstrating a secure, multi-tenant, multi-agent architecture using the Agents for AI (A2A) specification and the Google Agent Development Kit (ADK). It features a central orchestrator (host_agent) that routes user requests to specialized downstream agents, handling OAuth 2.0 authentication and automatic token refresh.
The system is composed of a central orchestrator and several independent services and agents.
host_agent: The central orchestrator and single entry point for users. It uses an LLM to understand user prompts and route them to the appropriate downstream agent. It also manages the security flow, including the OAuth 2.0 token lifecycle, and persists session and task state to a local SQLite database.- Downstream Agents:
airbnb_agent: Searches for accommodations.calendar_agent: Checks the user's Google Calendar.weather_agent: Provides weather forecasts.horizon_agent: A sample tenant-specific agent for retrieving order status. This agent is secure and requires an OAuth 2.0 token.
auth_lib: A shared library responsible for JWT validation, used by all secure downstream agents to protect their endpoints.idp: A mock OAuth 2.0 Identity Provider that issues short-lived JWT access tokens and long-lived refresh tokens.demo_agent_registry: A service discovery mechanism that provides thehost_agentwith the necessary endpoint information for all downstream agents.
It is highly recommended to use a Python virtual environment to manage dependencies.
-
Create a virtual environment:
python -m venv .venv
-
Activate the virtual environment:
- On macOS and Linux:
source .venv/bin/activate - On Windows:
.\.venv\Scripts\activate
- On macOS and Linux:
-
Install the project and its dependencies: From the project root directory, run the following command. The
-eflag installs the project in "editable" mode, which is crucial for allowing the various agent modules to be discovered correctly.pip install -e .
The system's security is based on the OAuth 2.0 Authorization Code Grant flow, enhanced with an automatic refresh token mechanism to handle expired access tokens seamlessly.
sequenceDiagram
participant User
participant Host Agent
participant IDP
participant Downstream Agent
User->>Host Agent: "what is the status of order 123"
Host Agent-->>User: "Please authenticate. [Follow this link]"
User->>IDP: Clicks link, logs in, grants consent
IDP-->>Host Agent: Redirects to /callback with auth_code
Host Agent->>IDP: Exchanges auth_code for tokens
IDP-->>Host Agent: Returns short-lived access_token and refresh_token
Note over Host Agent: Host Agent stores tokens in session state (DB)
User->>Host Agent: "done"
Host Agent->>Downstream Agent: Request with expired Bearer token
Downstream Agent-->>Host Agent: Error: "Token has expired"
Host Agent->>IDP: Sends refresh_token to /generate-token
IDP-->>Host Agent: Returns new access_token
Note over Host Agent: Host Agent updates session with new token
Host Agent->>Downstream Agent: Retries request with new Bearer token
Downstream Agent-->>Host Agent: Returns order status
Host Agent-->>User: "The status of order 123 is 'shipped'."
- Initial Authentication: When a user needs to access a secure agent (like the
horizon_agent), thehost_agentinitiates the OAuth 2.0 flow. The user is redirected to the mock Identity Provider (IDP) to log in. - Token Issuance: Upon successful login, the IDP issues a very short-lived access token (valid for only 10 seconds in this demo) and a long-lived refresh token. The
host_agentsecurely stores both in its session database. - Token Expiration: When the
host_agentattempts to use the access token after it has expired, the downstream agent rejects the request with a "token has expired" error. - Automatic Refresh: The
host_agentis designed to catch this specific error. It then uses the storedrefresh_tokento make a background request to the IDP's/generate-tokenendpoint. - Seamless Retry: The IDP provides a new access token. The
host_agentupdates its session state and automatically retries the original request to the downstream agent, which now succeeds. This entire refresh process is transparent to the end-user.
In a terminal, navigate to the idp directory and generate the necessary keys. This only needs to be done once.
(cd idp && python generate_jwks.py)This will create a private_key.pem file for signing tokens and a jwks.json file (the public key) within the idp directory.
To run the demo, you must start the mock IDP, the agent registry, and all agents in separate terminals. Run each of these commands from the project root directory.
Terminal 1: Start the IDP
python -m idp.appTerminal 2: Start the Agent Registry
python -m demo_agent_registry.appTerminal 3: Start the Weather Agent
python -m weather_agentTerminal 4: Start the Calendar Agent
python -m calendar_agentTerminal 5: Start the Horizon Agent (for a specific tenant)
python -m horizon_agent --port 10008 --tenant-id tenant-abcTerminal 6: Start the Airbnb Agent
python -m airbnb_agentTerminal 7: Start the Host Agent
# For tenant 'tenant-abc'
python -m host_agent --port 8083 --tenant-id tenant-abc- Open a browser and navigate to the
host_agent's Gradio UI at http://localhost:8083. - To test the secure flow, enter a prompt for the Horizon agent, for example:
what is the status of order 123 - You will be presented with a link to authenticate. Click the link.
- Log in to the mock IDP with username
john.doeand passwordpassword123. - Grant consent. You will be redirected back to the Gradio UI.
- After authenticating, signal the agent to continue by sending a message like "done" or re-submitting your original request. The agent will then use its stored credentials to complete the task. The token refresh flow will happen automatically in the background if the initial token expires.
-
A2A Task Delegation Lifecycle: To correctly create a task on a remote agent, the client MUST send a
SendMessageRequestwithout ataskId. The remote agent is responsible for creating the task and returning aTaskobject containing the newremote_task_id. The client must then capture this ID and associate it with its own local task record to maintain a link between the parent and child tasks. -
Accessing Request Headers: The A2A server framework does not expose request headers in the most intuitive location. They are not in
context.headersorcontext.call_context.headers. Instead, they are located within theServerCallContextobject atcontext.call_context.state['headers'], and all header keys are lowercased (e.g.,authorization). -
Testing with Secrets: To avoid committing private keys, a
test_config.pyfile (which is git-ignored) is used to store secrets for the test suite. An example file (test_config.example.py) is provided as a template. This is a simple but effective pattern for managing secrets in a test environment.
