(No changes to intro sections)
This project is a functional proof-of-concept demonstrating a secure, multi-tenant agent architecture.
- Primary Goal: The end-to-end security flow is working. A user can successfully authenticate via OAuth 2.0 to access a secure, tenant-specific agent.
- Task Lifecycle: The system now uses the A2A Task Lifecycle for seamless, stateful operations.
The architecture consists of a central Host Agent that acts as an orchestrator and several downstream agents that perform specific tasks.
- Host Agent: The single entry point for users. It uses a Gemini model to understand user prompts and route them to the appropriate downstream agent. It also manages the security flow 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 for JWT validation, used by all secure downstream agents.
- IDP (Identity Provider): A mock OAuth 2.0 server that issues JWTs.
- Agent Registry: A simple service that allows the Host Agent to discover available downstream agents.
The host_agent
uses a PersistentTaskStore
backed by a local SQLite database (host_agent.db
) to maintain the state of all user interactions.
- Task Lifecycle Management: When the
host_agent
delegates a task to a downstream agent, it first creates a record in its own database. It then updates this record with theremote_task_id
provided by the downstream agent, effectively linking the parent and child tasks. - Session Management: The system also persists session data, including OAuth tokens, to the database. This ensures that if the
host_agent
is restarted, it can resume interactions without requiring the user to re-authenticate.
(No changes to this section)
The security of the system is based on the OAuth 2.0 Authorization Code Grant flow. This ensures that a user's credentials are never exposed to the agents and that access to secure downstream agents is strictly controlled by short-lived access tokens.
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 access_token
IDP-->>Host Agent: Returns access_token
Note over User, Host Agent: User signals to continue (e.g., "done")
User->>Host Agent: "done"
Host Agent->>Downstream Agent: GET /orders/123 (with Bearer token)
Downstream Agent-->>Host Agent: Returns order status
Host Agent-->>User: "The status of order 123 is 'shipped'."
In a terminal, navigate to the idp
directory and generate the necessary keys.
(cd idp && python generate_jwks.py)
This will create a private_key.pem
file for signing tokens and a jwks.json
file for 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.app
Terminal 2: Start the Agent Registry
python -m demo_agent_registry.app
Terminal 3: Start the Weather Agent
python -m weather_agent
... Terminal 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.doe
and 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.
This project revealed several critical, non-obvious details about the A2A protocol and the ADK framework:
-
A2A Task Delegation Lifecycle: To correctly create a task on a remote agent, the client MUST send a
SendMessageRequest
without ataskId
. The remote agent is responsible for creating the task and returning aTask
object 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. -
A2AClient Authentication: The
a2a.client.A2AClient
does not handle auth tokens directly on a per-request basis. To make an authenticated call, a new, temporaryhttpx.AsyncClient
must be created with theAuthorization
token in itsheaders
. This temporary client is then used to create a new, single-useA2AClient
for the authenticated request. -
Accessing Request Headers: The A2A server framework does not expose request headers in the most intuitive location. They are not in
context.headers
orcontext.call_context.headers
. Instead, they are located within theServerCallContext
object atcontext.call_context.state['headers']
, and all header keys are lowercased (e.g.,authorization
).