diff --git a/a2a/iag-mcp-demo/README.md b/a2a/iag-mcp-demo/README.md
index e2c48e0..1d1408c 100644
--- a/a2a/iag-mcp-demo/README.md
+++ b/a2a/iag-mcp-demo/README.md
@@ -14,6 +14,14 @@ Contains some configuration yaml files for the open-source AgentGateway tool. Se
A simple Web GUI app and A2A client that enables Human-user prompting and interactions. The `chatbot` communicates directly with the `orchestrator_agent` (see below).
+The console header's **Architecture** link opens `/static/architecture.html`: a
+self-contained diagram of every container (console, gateways, agents, MCP
+servers, database), the IndyKite platform services, the IdP with its OAuth2
+token-exchange service, the external parties (Salesforce, Google Drive,
+Open-Meteo, the LLM) and the numbered request flow between them. It reads
+`/api/config` to badge the active usecase and grey out the `drive`/`crm`/`erp`
+groups when their compose profile is off.
+
## `orchestrator_agent`
An A2A agent that manages the interactions with the end-users, and delegates the action to further agents down the workflow chain: `query_retriever` (data/IKG), `query_weather` (weather), and `query_drive` (Google Drive, via the `analyst_agent` - enabled when `ANALYST_HOST` is set).
diff --git a/a2a/iag-mcp-demo/chatbot/app.py b/a2a/iag-mcp-demo/chatbot/app.py
index 885e4d7..06b2d7e 100644
--- a/a2a/iag-mcp-demo/chatbot/app.py
+++ b/a2a/iag-mcp-demo/chatbot/app.py
@@ -57,6 +57,10 @@
APP_AGENT_CREDENTIALS_TOKEN = (os.getenv("APP_AGENT_CREDENTIALS_TOKEN") or "").strip()
EXPLAIN_STAFF_QUERY_ID = (os.getenv("EXPLAIN_STAFF_QUERY_ID") or "").strip()
EXPLAIN_DIRECT_QUERY_ID = (os.getenv("EXPLAIN_DIRECT_QUERY_ID") or "").strip()
+# Architecture page: active usecase + compose profiles, used only to badge the
+# diagram and dim the optional (profile-gated) service groups.
+USECASE = (os.getenv("USECASE") or "").strip()
+COMPOSE_PROFILES = [p.strip() for p in (os.getenv("COMPOSE_PROFILES") or "").split(",") if p.strip()]
EXPLAIN_ENABLED = all(
(INDYKITE_BASE_URL, APP_AGENT_CREDENTIALS_TOKEN, EXPLAIN_STAFF_QUERY_ID, EXPLAIN_DIRECT_QUERY_ID),
)
@@ -169,10 +173,22 @@ def health():
)
+def _id_token_display_name() -> str:
+ """Best-effort display name from the session's id_token (unverified decode, cosmetic only)."""
+ token = session.get("id_token") or ""
+ try:
+ payload = token.split(".")[1]
+ claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
+ except Exception: # opaque/absent token: no name to show
+ return ""
+ return str(claims.get("preferred_username") or claims.get("name") or claims.get("email") or claims.get("sub") or "")
+
+
@app.route("/api/auth/status", methods=["GET"])
def auth_status():
- """Return whether the user is authenticated."""
- return jsonify({"logged_in": bool(session.get("access_token"))})
+ """Return whether the user is authenticated (and who, for the header chip)."""
+ logged_in = bool(session.get("access_token"))
+ return jsonify({"logged_in": logged_in, "username": _id_token_display_name() if logged_in else ""})
@app.route("/api/auth/login", methods=["GET"])
@@ -376,6 +392,9 @@ def get_config():
# the map derives a DENY card's workflow id from its gateway name.
"explain_enabled": EXPLAIN_ENABLED,
"explain_workflow_map": EXPLAIN_WORKFLOW_MAP,
+ # architecture page: badge + dimming of profile-gated groups
+ "usecase": USECASE,
+ "profiles": COMPOSE_PROFILES,
},
)
diff --git a/a2a/iag-mcp-demo/chatbot/static/architecture.html b/a2a/iag-mcp-demo/chatbot/static/architecture.html
new file mode 100644
index 0000000..b4fe891
--- /dev/null
+++ b/a2a/iag-mcp-demo/chatbot/static/architecture.html
@@ -0,0 +1,403 @@
+
+
+
+ Every hop between containers goes through an IndyKite Agent Gateway (orange): the caller's token is introspected,
+ the call is authorized against the knowledge graph, a fresh delegation token is minted for the next hop, and an
+ audit event is emitted. Greyed-out groups are optional compose profiles that are currently off.
+
+
+
+
+
+
+
+
+
The flow
+
+
Login & prompt: the user logs in at the console through the IdP's OAuth2 Authorization Code flow, comes back with a user access token, and sends a prompt.
+
IdP token services: the same IdP that authenticated the user also runs the token-exchange service (RFC 8693) that every gateway calls to mint a fresh delegation token — user as subject, agent chain in act - for the next hop.
+
Console → orchestrator: each prompt is forwarded with the user's token to orchestrator-iag.
+
Gateway checks (every hop): the gateway introspects the token, matches the call's actor chain against a Workflow in the knowledge graph, runs the AuthZEN CAN_TRIGGER check, emits an audit event, and exchanges the token (step 2) before forwarding.
+
Orchestrator routes: the LLM picks a tool; the A2A call goes to the chosen agent's gateway, where the same checks run again on the new chain.
+
Agents fetch data: retriever/analyst/weather use MCP through mcp-iag to the hosted IndyKite MCP server; the analyst additionally reaches Google Drive via drive-mcp-iag and the ERP via erp-mcp-iag.
+
External parties: Google Drive (server-side OAuth identity), Salesforce (the crm agent swaps its IndyKite delegation token for a Salesforce access token via RFC 7523 JWT Bearer — a second, independent trust domain), Open-Meteo, and the configured LLM.
+
Data-plane authorization: downstream services can ask the platform what the calling user may see before answering - e.g. erp-mcp asks AuthZEN search/resource which resources the user may CAN_VIEW and returns only those rows. The backend lives on a private network whose sole bridge is its gateway.
+
Back to the console: agents report every minted token as a TOKEN card and gateways' decisions appear in the audit-log terminal; deny cards offer a why? graph explanation (ContX IQ).
+
+
+
+
Legend
+
+
IndyKite Agent Gateway (IAG) container
+
Agent container (A2A / LangChain)
+
Console / user
+
Data-plane service (MCP server, database)
+
External party (SaaS / public API)
+
Control plane: OAuth2, introspection, AuthZEN, audit