098 Architecture: Enterprise RBAC for CAIPE Platform
Spec: 098 β Enterprise RBAC for Slack and CAIPE UI (098-enterprise-rbac-slack-ui)
Date: April 2026
Supersedes: 093 architecture (historical reference)
Canonical Architecture Diagramβ
This is the single source of architecture truth for Enterprise RBAC. Numbered arrows correspond to the flow table below the diagram.
Flow Tableβ
| Step | From | To | Description |
|---|---|---|---|
| β | User | Slack / Webex / Admin UI | User sends command or performs admin action |
| β‘ | Entry point | Bot Backend / BFF | Event delivered with user context (Slack signature, OAuth, NextAuth session) |
| β’a | Enterprise IdP | Keycloak | Federation: Okta/Entra groups and identity brokered into Keycloak |
| β’b | Bot/BFF | Keycloak | OBO token exchange (RFC 8693): bot obtains JWT with sub=user, act=bot, groups, roles, scope, org |
| β£a | Bot/BFF | Keycloak AuthZ (PDP) | UI/Slack authorization: checks JWT + requested capability against 098 matrix β allow/deny |
| β£b | Bot/BFF | Agent Gateway | MCP/A2A/agent authorization: AG validates JWT, applies CEL policy β allow/deny |
| β€a | Agent Gateway | Supervisor β Agents | Authorized request routed to domain agents |
| β€b | Agents | Agent Gateway β MCP | Agent MCP tool calls re-enter AG for tool-level RBAC (FR-016) |
| β₯ | PDP / RAG | MongoDB + Keycloak | Hybrid store: Keycloak holds authz policies (resources, scopes, permissions) and Slack identity links (user attributes); MongoDB holds team/KB assignments, ASP policies, app metadata |
| β¦ | Agents | GitHub / Jira / ArgoCD | Downstream API calls using brokered user tokens |
Authorization Enforcement Pointsβ
098 defines three enforcement zones, each with its own PDP:
| Zone | Enforcement Point | PDP | Traffic |
|---|---|---|---|
| UI | Next.js BFF (NextAuth middleware) | Keycloak Authorization Services | Admin UI API routes, page access |
| Slack / Webex | Bot backend middleware | Keycloak Authorization Services | Slack commands, Webex events |
| MCP / A2A / Agent | Agent Gateway (required) | AG built-in (CEL policy) | MCP tool calls, A2A tasks, agent dispatch |
All three zones enforce the same 098 permission matrix (FR-014). Default deny applies everywhere (FR-002).
Sequence Diagram 1: Slack Identity Linking (FR-025)β
One-time flow to establish the slack_user_id β keycloak_sub mapping. The mapping is stored as a Keycloak user attribute β the bot has no MongoDB dependency.
Sequence Diagram 2: Authorized Request Flowβ
Every subsequent request after identity linking. Shows OBO exchange, PDP check, and agent execution.
IdP Groups β Keycloak Roles Mapping (FR-010)β
Enterprise IdP groups (Okta and Microsoft Entra ID / AD-backed groups) are mapped to CAIPE platform roles at token issuance time inside Keycloak β no runtime SCIM sync or directory lookups. Keycloak acts as a required OIDC broker that federates both IdPs.
Sequence Diagram 3: IdP Groups β Keycloak Roles (Runtime)β
This diagram shows what happens at login time when a user authenticates through a federated IdP. It covers both the SAML path (Okta SAML, Entra SAML) and the OIDC path (Okta OIDC).
Keycloak Mapper Configuration (One-Time Admin Setup)β
Three layers of mappers work together to transform IdP groups into JWT claims:
| Layer | Mapper Type | Keycloak Config | Purpose |
|---|---|---|---|
| 1. Import | Identity Provider Mapper | SAML: "Attribute Importer" β attribute name groups β user attribute idp_groups | Extracts groups from IdP assertion/token into Keycloak user profile |
OIDC: "Claim to User Attribute" β claim groups β user attribute idp_groups | |||
| 2. Map to Roles | Identity Provider Mapper | "Hardcoded Role" or "SAML Attribute IdP Role Mapper" β when groups contains platform-admin β assign KC realm role admin | Converts IdP group membership into Keycloak realm roles |
| Repeat per group β role mapping | |||
| 3. Emit in JWT | Client Protocol Mapper | "Group Membership" β token claim groups | Emits groups in JWT for downstream consumers |
"Realm Role" β token claim roles | Emits mapped roles in JWT | ||
"User Attribute" β token claim org | Emits tenant/org context in JWT |
IdP-Specific Federation Setupβ
| IdP | Protocol | Group Source | Keycloak Broker Config |
|---|---|---|---|
| Okta (SAML) | SAML 2.0 | SAML Assertion β Attribute Statement groups | Identity Provider β SAML β Import SAML attributes |
| Okta (OIDC) | OIDC | ID Token β groups claim (requires Okta "Groups claim" config in the Okta app) | Identity Provider β OIDC β Import OIDC claims |
| Microsoft Entra ID (SAML) | SAML 2.0 | SAML Assertion β http://schemas.microsoft.com/ws/2008/06/identity/claims/groups (GUIDs) or custom groups attribute | Identity Provider β SAML β Attribute Importer (map GUIDs or group names) |
| Microsoft Entra ID (OIDC) | OIDC | ID Token β groups claim (requires Entra "Group claims" config in App Registration β Token configuration) | Identity Provider β OIDC β Claim to User Attribute |
Entra ID note: By default Entra sends group Object IDs (GUIDs) in SAML/OIDC. To get human-readable names, configure "Emit groups as role claims" or use the
cloud_displayNamesource attribute in the enterprise app's SAML claims configuration.
Group β Role Mapping Tableβ
| IdP Group | Keycloak Realm Role | Capabilities (098 matrix examples) |
|---|---|---|
platform-admin | admin | All protected capabilities |
team-a-eng | team_member(team-a) | Chat, invoke team-a tools, query team-a KBs |
kb-admins | kb_admin | Create/update/delete KBs, manage ingest |
team-b-ops | team_member(team-b) | Chat, invoke team-b tools, query team-b KBs |
| (no group) | (no role) | Default deny β no platform access (FR-002) |
Resulting JWT Claims (Example)β
{
"sub": "a1b2c3d4-...",
"iss": "https://keycloak.caipe.example.com/realms/caipe",
"aud": "caipe-platform",
"groups": ["platform-admin", "team-a-eng"],
"roles": ["admin", "team_member(team-a)"],
"org": "acme-corp",
"scope": "openid profile caipe",
"exp": 1743900000
}
The CAIPE platform, Agent Gateway, and Keycloak PDP all consume these JWT claims directly β no callback to the IdP or Keycloak at authorization time.
Multi-Tenant Isolation (FR-020)β
βββββββββββββββββββββββββββββββββββ
β Tenant Boundary β
β βββββββββββββ βββββββββββββ β
β β Org A β β Org B β β
β β β β β β
β β Users A β β Users B β β
β β Agents A β β Agents B β β
β β Tools A β β Tools B β β
β β KBs A β β KBs B β β
β β β β β β
β β JWT.org=A β β JWT.org=B β β
β βββββββββββββ βββββββββββββ β
β β
β PDP + AG enforce: principal β
β in org A CANNOT access org B β
β resources (FR-020) β
βββββββββββββββββββββββββββββββββββ
Slack Identity Linking (FR-025)β
The identity linking flow establishes the slack_user_id β keycloak_sub mapping required before any OBO exchange. The mapping is stored as a custom Keycloak user attribute β the Slack bot has no MongoDB dependency.
Storage mechanismβ
| Aspect | Detail |
|---|---|
| Where | Keycloak user profile β custom attribute slack_user_id |
| Write (linking) | Bot calls Keycloak Admin API to set slack_user_id on the authenticated user |
| Read (lookup) | Bot calls Keycloak Admin API to find user by attribute slack_user_id = X β returns keycloak_sub |
| Bot dependencies | Keycloak only (Admin API + OIDC); no MongoDB on the Slack path |
Flowβ
ββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Slack β β Slack Bot β β Keycloak β β Enterprise β
β User β β Backend β β (broker) β β IdP (Okta) β
ββββββ¬ββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β 1. First β β β
β command βββββββΆβ β β
β β 2. Admin API: β β
β β find user by β β
β β slack_user_id=X βββΆβ β
β ββββ Not found βββββββ β
β β β β
βββββ 3. "Link βββ β β
β account" URL β β β
β (single-use, β β β
β time-bounded) β β β
β β β β
βββ 4. Click βββββββββββββββββββββββββΆβ β
β URL β βββ 5. Federate βββΆβ
β β ββββ 6. SAML βββββββ
ββββββ 7. Auth ββββββββββββββββββββββββ β
β success β β β
β ββββ 8. Callback βββββ β
β β (keycloak_sub) β β
β β β β
β β 9. Admin API: set β β
β β slack_user_id=X on β β
β β keycloak_sub ββββββΆβ β
β ββββ OK ββββββββββββββ β
βββ 10. "Linked!" β β β
β β β β
β 11. Subsequent β β β
β commands ββββββΆβ 12. Admin API: β β
β β find slack_user_id β β
β β β keycloak_sub ββββΆβ β
β ββββ keycloak_sub ββββ β
β β 13. OBO exchange β β
β β (RFC 8693) ββββββββΆβ β
β ββββ JWT βββββββββββββ β
Security constraints: Linking URL is single-use, time-bounded (short TTL), HTTPS-only. Unlinked users are denied all RBAC-protected operations.
RBAC Configuration Store (FR-023)β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CAIPE Admin UI (FR-024) β
β Administrators manage RBAC here β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Roles & Access Tab (US6) β β
β β β’ Create/delete custom realm roles β β
β β β’ Map IdP groups β realm roles β β
β β β’ Assign roles to teams β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββ¬βββββββββββββββββββββββββββ¬ββββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββββ ββββββββββββββββββββββββ
β Keycloak β β MongoDB β
β (Admin REST API) β β β
β β β β
β β’ Resources β β β’ Team/KB ownership β
β (components) β β assignments β
β β’ Scopes β β β’ Custom RAG tool β
β (capabilities) β β bindings β
β β’ Policies β β β’ App metadata β
β (role-based) β β β’ ASP tool policies β
β β’ Realm Roles β β β’ Team keycloak_ β
β (CRUD via UI) β β roles assignments β
β β’ IdP Mappers β β β
β (groupβrole, UI) β β β
β β’ Permissions β β β
β β’ User attributes β β β
β (slack_user_id) β β β
β (FR-025) β β β
β β β β
β PDP for UI/Slack β β Operational state β
ββββββββββββββββββββββββ ββββββββββββββββββββββββ
Admin UI β Keycloak Admin API Flow (FR-024, US6)β
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β Admin UI β β BFF API Routes β β Keycloak Admin β
β RolesAccessTab ββββΆβ /api/admin/ ββββΆβ REST API β
β β β roles, β β β
β CreateRole β β role-mappings, β β client_creds β
β Dialog β β teams/:id/roles β β grant auth β
β β β β β β
β GroupMapping β β requireAdmin() β β realm-managementβ
β Dialog β β session check β β service account β
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
OBO Delegation Chain (FR-018, FR-019)β
The multi-hop delegation chain ensures the originating user is always the effective principal:
User βββΆ Slack Bot βββΆ Supervisor βββΆ Agent βββΆ MCP Tool
β β β β β
β OBO exchange Forwards Forwards AG checks
β (RFC 8693) user JWT user JWT JWT.sub=user
β β β β β
ββββ sub=user βββββββββββββββββββββββββββββββββββββββ
act=bot
scope=user's entitlements (not bot's)
groups=[user's groups]
org=user's org
Effective permissions = intersection of:
β’ User's entitlements (098 matrix)
β’ Bot service account's scope ceiling
β’ Component's matrix row (FR-008)
PDP Architecture (FR-022)β
Keycloak is required (Session 2026-04-03). Enterprise IdPs (Okta, Entra, SAML) federate into Keycloak via identity brokering.
| Path | PDP | How |
|---|---|---|
| UI / Slack / Webex | Keycloak Authorization Services | UMA / resource-based permissions; 098 matrix modeled as KC resources, scopes, policies |
| MCP / A2A / Agent | Agent Gateway | CEL policy; JWT issued by Keycloak |
Keycloak Authorization Services:
- Consume JWT
groups,roles,scope,orgclaims (FR-010) - 098 permission matrix modeled as Keycloak resources (components), scopes (capabilities), and policies (role-based)
- Return allow/deny with audit-grade detail (FR-005)
- Target sub-5ms decision latency
- Admin manages policies via Keycloak Admin Console or CAIPE Admin UI (which calls Keycloak Admin API)
Map RAG RBAC to Keycloak + Per-KB Access Control Architecture Overviewβ
The RAG server is integrated into the Keycloak RBAC system with defense-in-depth enforcement. The BFF performs coarse Keycloak AuthZ checks; the RAG server validates the JWT directly and enforces per-KB access control. This section documents the architecture for FR-026 (Keycloak JWT integration) and FR-027 (per-KB access control).
Dual-Layer Enforcement Flowβ
Keycloak Realm Role to RAG Server Role Mapping (FR-026)β
The RAG server maps Keycloak realm roles from the JWT roles claim to its internal role hierarchy. When the roles claim is present, Keycloak role mapping takes precedence. When absent, the existing group-based assignment (RBAC_*_GROUPS) is used as fallback.
| Keycloak Realm Role | RAG Server Role | Permissions | KB Access |
|---|---|---|---|
admin | admin | read, ingest, delete | All KBs (global override) |
kb_admin | ingestonly | read, ingest | All KBs (global override) |
team_member | readonly | read | Team-owned KBs only |
chat_user | readonly | read | Per-KB roles or team-owned KBs |
kb_reader:<kb-id> | readonly (scoped) | read | Specified KB only |
kb_reader:* | readonly (all) | read | All KBs (wildcard) |
kb_ingestor:<kb-id> | ingestonly (scoped) | read, ingest | Specified KB only |
kb_ingestor:* | ingestonly (all) | read, ingest | All KBs (wildcard) |
kb_admin:<kb-id> | admin (scoped) | read, ingest, delete | Specified KB only |
| (no matching role) | anonymous | (none) | No KBs |
Per-KB Access Resolution (FR-027)β
Effective KB access is the union of Keycloak per-KB roles and team ownership. Global roles override per-KB restrictions.
Query-Time KB Filteringβ
The /v1/query endpoint injects a datasource_id filter into vector DB queries based on the user's accessible KB list. This is server-side enforced and transparent to the caller β the API consumer does not need to know which KBs they can access.
User calls POST /v1/query { "query": "how do I deploy?", "filters": {} }
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β RAG Server /v1/query handler β
β β
β 1. Validate JWT β UserContext (role, kb_perms) β
β 2. require_role(Role.READONLY) β β
β 3. get_accessible_kb_ids(user_context) β
β β ["kb-team-a", "kb-platform"] β
β 4. inject_kb_filter(query, accessible_kbs) β
β β filters.datasource_id IN [...] β
β 5. VectorDBQueryService.query(filtered_request) β
β β results from kb-team-a + kb-platform only β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Defense-in-Depth Enforcement Layersβ
Four layers of authorization checks protect KB operations:
| Layer | Check | PDP | Scope | Failure Mode |
|---|---|---|---|---|
1. BFF /api/rag/kb/* | requireRbacPermission("rag", "kb.query") | Keycloak AuthZ | Coarse capability (can user do KB operations at all?) | 401/403 to UI |
| 2. RAG global role | require_role(Role.READONLY) via JWT β Keycloak role mapper | RAG server (JWT) | Global role check (is user authenticated with sufficient role?) | 401/403 from RAG |
| 3. RAG per-KB access | require_kb_access(kb_id, scope) | RAG server (JWT + MongoDB) | Fine-grained per-KB (can user access THIS specific KB?) | 403 from RAG |
| 4. RAG query filter | inject_kb_filter() in /v1/query | RAG server (JWT + MongoDB) | Query-time row filtering (restrict results to accessible KBs) | Empty results / 503 |
If any layer denies, the operation is denied. If MongoDB is unavailable for team ownership lookup, the system fails closed (deny).
Dynamic Agent RBAC Architecture (FR-028, FR-029, FR-030)β
Dynamic agents are governed by the same Keycloak RBAC model as KBs and tools. This section documents the three-layer authorization model, CEL as the universal policy engine, and deepagent MCP routing through Agent Gateway.
Three-Layer Enforcement Flowβ
Dynamic Agent Role Mapping Tableβ
| Keycloak Realm Role | Scopes Granted | Agent Access |
|---|---|---|
admin | view, invoke, configure, delete | All agents (global override) |
agent_admin:<agent-id> | view, invoke, configure, delete | Specified agent only |
agent_admin:* | view, invoke, configure, delete | All agents (wildcard) |
agent_user:<agent-id> | view, invoke | Specified agent only |
agent_user:* | view, invoke | All agents (wildcard) |
team_member(team-x) | view, invoke (team agents) | Agents with visibility: team + shared_with_teams includes team-x |
| (owner) | view, invoke, configure, delete | Own agents (owner_id match) |
| (no matching role) | (none) | Only visibility: global agents |
Per-Agent Access Resolutionβ
Effective agent access is the union of three sources: per-agent Keycloak roles, MongoDB visibility, and ownership. CEL evaluates all three at runtime.
CEL as Universal Policy Engine (FR-029)β
CEL is mandated at all four enforcement points. Each service embeds a CEL evaluator library with a shared context schema.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CEL Context Schema β
β β
β user.roles : ["admin", "agent_user:agent-123", ...] β
β user.teams : ["team-a", "team-b"] β
β user.email : "user@corp.com" β
β resource.id : "agent-123" | "kb-team-a" | ... β
β resource.type : "dynamic_agent" | "kb" | "rag_tool" β
β resource.visibility : "private" | "team" | "global" β
β resource.owner_id : "owner@corp.com" β
β resource.shared_with_teams : ["team-a"] β
β action : "view" | "invoke" | "configure" | ... β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββ
β Agent β β RAG Server β β Dynamic β β BFF β
β Gateway β β (Python) β β Agents β β (TS) β
β β β β β (Python) β β β
β CEL built-in β β cel-python β β cel-python β β cel-js β
β (Rust) β β β β β β β
β β β Per-KB β β Per-agent β β RBAC β
β MCP/A2A/ β β access β β access β β middlewareβ
β agent policy β β (FR-027) β β (FR-028) β β checks β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββ
Example CEL expressions (configurable, not hardcoded):
// Dynamic agent view access
user.roles.exists(r, r == "admin")
|| user.roles.exists(r, r == "agent_user:" + resource.id)
|| user.roles.exists(r, r == "agent_user:*")
|| resource.visibility == "global"
|| (resource.visibility == "team"
&& resource.shared_with_teams.exists(t, t in user.teams))
|| resource.owner_id == user.email
// KB read access (same pattern)
user.roles.exists(r, r == "admin" || r == "kb_admin")
|| user.roles.exists(r, r == "kb_reader:" + resource.id)
|| user.roles.exists(r, r == "kb_reader:*")
|| resource.team_owned_by.exists(t, t in user.teams)
Deepagent MCP Routing (FR-030)β
Slack Channel-to-Team RBAC (FR-031, FR-032)β
Slack channels act as team selectors β providing context for which team's resources (KBs, agents, tools) are in scope. The channel does not grant additional permissions; the user's Keycloak roles are the sole authority.
Slack Bot RBAC Flow with Channel Contextβ
Slack Bot Data Sourcesβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Slack Bot Runtime β
β β
β ββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Keycloak β β MongoDB β β
β β β β β β
β β β’ Identity β β β’ Channel-to-team β β
β β linking β β mappings (FR-031) β β
β β (FR-025) β β β β
β β β’ OBO token β β β’ Operational metrics β β
β β exchange β β (last interaction, β β
β β (FR-018) β β OBO success/fail) β β
β β β’ AuthZ PDP β β β β
β β (FR-022) β β Cached in bot memory β β
β β β β with 60s TTL β β
β ββββββββββββββββ ββββββββββββββββββββββββββββ β
β β
β Identity & Auth βββ Keycloak (source of truth) β
β Team Context βββ MongoDB (channel mappings) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Admin UI Slack Management Dashboard (FR-032)β
Component Summaryβ
| Component | Role | Required? | Authorization |
|---|---|---|---|
| Slack / Webex | User-facing entry (at least one) | At least one channel | Bot validates identity + PDP check |
| CAIPE Admin UI | Admin web interface | Yes | NextAuth session + PDP check |
| Slack Bot / Webex Bot Backend | Event handling, identity resolution (via Keycloak user attributes), OBO exchange | Yes | PDP for capability checks; no MongoDB dependency |
| Keycloak (required) | OIDC broker, token issuance, groupsβroles mapping, OBO, Authorization Services (PDP), Slack identity link storage (user attributes) | Required | Source of JWT claims + PDP for UI/Slack + identity link store |
| Enterprise IdP | SSO (Okta SAML, Entra AD); federated into Keycloak | Optional | Federation source |
| Agent Gateway | MCP/A2A gateway, JWT validation, CEL policy | Required | PDP for agent traffic |
| Supervisor / Orchestrator | A2A server, agent routing | Yes | Carries forwarded identity |
| Domain Agents | GitHub, Jira, ArgoCD, etc. | Yes | OBO tokens for downstream |
| MCP Servers | Tool invocation | Yes | AG-gated access |
| RAG Server | KBs, datasources | Yes | PDP-gated admin; AG-gated queries; CEL per-KB access (FR-027) |
| Dynamic Agents | User-created/runtime agents, deepagent LangGraph | Yes | Three-layer RBAC: Keycloak resource + per-agent roles + MongoDB visibility + CEL (FR-028); MCP calls via AG (FR-030) |
| Slack Bot | Slack commands, identity linking, channel-team scoping | Yes | Identity linking via Keycloak (FR-025); OBO exchange (FR-018); channel-to-team mapping from MongoDB with 60s cache (FR-031); AuthZ via Keycloak PDP (FR-022); Admin UI dashboard (FR-032) |
| MongoDB | Users, policies, permission matrix, team/KB config (no Slack identity links β those are in Keycloak) | Yes | Data store for PDP + Admin UI |
Fail-Closed Behaviorβ
| Failure | Impact | Behavior |
|---|---|---|
| Agent Gateway down | MCP/A2A/agent traffic | Denied (fail closed). Slack and Admin UI unaffected. |
| Keycloak down | Token issuance, login, UI/Slack authz | No new sessions; existing valid JWTs may continue until expiry; authz checks denied (fail closed). |
| MongoDB unavailable | Matrix/config reads | PDP returns deny (fail closed). |
FR-038: Team-Based KB RBAC + Agent Gateway MCP Routingβ
Overviewβ
FR-038 introduces team-based KB access control with Agent Gateway MCP routing, OBO token propagation, and per-session auth-aware supervisor tools.
Architecture Diagramβ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CAIPE UI (Next.js) β
β ββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββ β
β β KB Browser β β Admin Teams β β Agent Chat (A2A SDK) β β
β β (IngestView)β β (KB Assign) β β accessToken in Bearer β β
β βββββββ¬βββββββ ββββββββ¬ββββββββ ββββββββββββββ¬ββββββββββββββ β
β β β β β
ββββββββββΌβββββββββββββββββΌβββββββββββββββββββββββββΌβββββββββββββββββββ
β REST β REST β A2A JSON-RPC
βΌ βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BFF API Routes (Next.js) β
β βββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββββββ β
β β /api/rag/kb/* β β /api/admin/teams β β /api/a2a/β¦ β β
β β adds X-Team-Idβ β /[id]/kb-assign β β forwards Bearer β β
β β header β β CRUD on MongoDB β β to supervisor β β
β βββββββββ¬ββββββββ ββββββββ¬ββββββββββββ βββββββββββ¬βββββββββββββ β
β β β β β
β βΌ βΌ β β
β βββββββββββββ ββββββββββββββββ β β
β β RAG Serverβ β MongoDB β β β
β β REST API β β team_kb_ β β β
β β (direct) β β ownership β β β
β βββββββββββββ ββββββββββββββββ β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Supervisor (A2A Server) β
β βββββββββββββββ ββββββββββββββββββββββββββββββ β
β β Extract user β β OBO Token Exchange β β
β β Bearer from ββββ POST /realms/β¦/protocol/ β β
β β HTTP request β β openid-connect/token β β
β β β β grant_type=token-exchange β β
β ββββββββββββββββ β subject_token=user_jwt β β
β β β OBO JWT (sub=user,act=svc) β β
β ββββββββββββ¬ββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββ β
β β Auth-Aware Proxy Tools (wrap_rag_tools_with_auth) β β
β β ββββββββββββββββββββββββ β β
β β β Original RAG Tool β Reads obo_token from β β
β β β (from compiled graph)β RunnableConfig.configurable β β
β β β name + schema kept β βββΊ per-invocation MCP client β β
β β ββββββββββββββββββββββββ with Bearer auth β β
β βββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Agent Gateway (AG) β
β ββββββββββββββββ ββββββββββββββββββ ββββββββββββββββββββββββββ β
β β JWT Validate β β CEL Policy β β Target: rag β β
β β (Keycloak β β Evaluation β β mcp: β β
β β JWKS) β β (tool-level β β host: rag_server: β β
β β β β authz) β β 9446/mcp β β
β ββββββββ¬ββββββββ βββββββββ¬βββββββββ βββββββββββ¬βββββββββββββββ β
β β β β β
β ββββββββββββββββββββ΄βββββββββββββββββββββββ β
β β Authorized β
β βΌ β
ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RAG Server β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β rbac.py: β β
β β - Extract team_id from JWT roles or X-Team-Id header β β
β β - Query MongoDB team_kb_ownership for allowed datasources β β
β β - Filter /v1/datasources and MCP tool responses β β
β β - Fail closed: if MongoDB unreachable β empty results β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Dynamic Agents β
β ββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββ β
β β AgentRuntime β β MCP Client (per-session) β β
β β auth_bearer = ββββ agent_gateway_url = AGENT_GATEWAY_URL β β
β β user OBO JWT β β headers: Authorization: Bearer <obo> β β
β ββββββββββββββββ ββββββββββββββββββββ¬βββββββββββββββββββββββ β
β β β
β Sub-agents inherit auth_bearer β β
β and agent_gateway_url β β
βββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β
βΌ
Agent Gateway
(same as above)
Data Flow: Team-Scoped KB Query via Supervisorβ
- User sends chat β UI attaches
accessTokenas Bearer header - BFF forwards Bearer to supervisor A2A endpoint
- Supervisor extracts user JWT from request, performs OBO token exchange with Keycloak (RFC 8693):
grant_type=urn:ietf:params:oauth:grant-type:token-exchange,subject_token=<user_jwt>β receives OBO JWT withsub=user, act.sub=caipe-platform - Auth-aware proxy tool is invoked by LangGraph; it reads
obo_tokenfromRunnableConfig.configurable, creates an ephemeral MCP client withAuthorization: Bearer <obo_token>, connects to AG - Agent Gateway validates the OBO JWT via Keycloak JWKS, evaluates CEL tool-level policies, forwards the MCP request to RAG server target
- RAG server extracts team membership from JWT roles (
team_member(<id>)), queriesteam_kb_ownershipin MongoDB for allowed datasource IDs, filters results, returns only team-authorized data
Data Flow: Team-Scoped KB Ingest via UIβ
- User navigates to KB β IngestView; UI calls
GET /api/rag/kb/v1/datasources - BFF proxy adds
X-Team-Idheader (from session JWT roles) and proxies to RAG server - RAG server checks
team_kb_ownershipβ if user's team hasingestoradminon the target datasource, allow; otherwise deny - UI hides ingest/delete buttons for KBs where the user's team lacks matching permissions; shows team-ownership badges
Fallback Behaviorβ
| Condition | Behavior |
|---|---|
AGENT_GATEWAY_URL unset | Supervisor + dynamic agents connect directly to MCP servers (no AG, no OBO) |
| OBO exchange fails | Supervisor falls back to service-account token (reduced access) |
MongoDB team_kb_ownership unreachable | RAG server returns empty results (fail-closed) |
| AG down | MCP calls denied; UI REST path unaffected |
FR-038h: KB UI Team Assignment Architectureβ
The Knowledge Base UI provides inline team access management through a reusable
KbTeamAccessPanel React component that operates in two modes (compact and full),
plus an optional team selector in the ingest form.
Component Architectureβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IngestView.tsx β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Ingest Form β β
β β ββββββββββββββ βββββββββββββββ ββββββββββββββββββ β β
β β β URL input β β Share with: β β Permission: β β β
β β β β β <select> β β <select> β β β
β β β β β (optional) β β read/ingest/ β β β
β β β β β β β admin β β β
β β ββββββββββββββ βββββββββββββββ ββββββββββββββββββ β β
β β β β
β β On success: POST ingest β PUT /api/admin/teams/{id}/ β β
β β kb-assignments (assign new datasource) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Datasource Row β β
β β βββββββββββββ βββββββββ ββββββββββββββββββββββββ ββββββββ β β
β β β Name β βBadges β βKbTeamAccessPanel β βType β β β
β β β β β(teams)β βmode="compact" β βbadge β β β
β β β β β β β(Users iconβpopover) β β β β β
β β βββββββββββββ βββββββββ ββββββββββββββββββββββββ ββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Expanded Datasource Detail β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β KbTeamAccessPanel mode="full" β β β
β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β β β Team Access β β β β
β β β β ββββββββββββββ¬βββββββββββ¬βββββββββ β β β β
β β β β β Team Name β Perm β Remove β β β β β
β β β β ββββββββββββββΌβββββββββββΌβββββββββ€ β β β β
β β β β β Platform β Ingest βΌ β π β β β β β
β β β β β DataSci β Read βΌ β π β β β β β
β β β β ββββββββββββββ΄βββββββββββ΄βββββββββ β β β β
β β β β ββββββββββββββββ¬βββββββββββ¬βββββββ β β β β
β β β β β Add team...βΌ β Perm βΌ β + β β β β β
β β β β ββββββββββββββββ΄βββββββββββ΄βββββββ β β β β
β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Data Flow: KB UI Team Assignmentβ
User clicks Share icon (compact) or views expanded detail (full)
β
βΌ
KbTeamAccessPanel
β
ββββΊ GET /api/admin/teams β list all teams
β
ββββΊ GET /api/admin/teams/{id}/kb-assignments (per team)
β β build: which teams have this datasource assigned?
β
ββββΊ User adds team:
β GET /api/admin/teams/{id}/kb-assignments (current state)
β PUT /api/admin/teams/{id}/kb-assignments (append datasource)
β β calls onUpdate() β reloadTeamKb()
β
ββββΊ User removes team:
β DELETE /api/admin/teams/{id}/kb-assignments?datasource_id=...
β β calls onUpdate() β reloadTeamKb()
β
ββββΊ User changes permission:
GET /api/admin/teams/{id}/kb-assignments (current state)
PUT /api/admin/teams/{id}/kb-assignments (update kb_permissions)
β calls onUpdate() β reloadTeamKb()
Data Flow: Post-Ingest Team Assignmentβ
User fills ingest form + selects team + permission
β
βΌ
handleIngest()
β
ββββΊ POST /api/rag/kb/v1/ingest (create datasource + job)
β β returns { datasource_id, job_id }
β
ββββΊ If ingestTeamId is set:
GET /api/admin/teams/{id}/kb-assignments (current state)
PUT /api/admin/teams/{id}/kb-assignments (append new datasource_id)
β reloadTeamKb()
New Filesβ
| File | Purpose |
|---|---|
ui/src/components/rag/KbTeamAccessPanel.tsx | Reusable panel (compact popover + full inline) for managing team-KB assignments per datasource |
Modified Filesβ
| File | Changes |
|---|---|
ui/src/components/rag/IngestView.tsx | Import KbTeamAccessPanel; add compact Share button per row; add full panel in detail; add team/permission selectors in ingest form; post-ingest team assignment |
ui/src/hooks/useTeamKbOwnership.ts | Already exports reload (used as reloadTeamKb) |
FR-038d: AG End-to-End + RAG MCP RBAC Enforcement Architectureβ
Problemβ
Team-based KB scoping was enforced only on the UI REST path (BFF sets X-Team-Id, RAG server calls inject_kb_filter). The Slack/supervisor MCP path bypassed all RBAC because:
AGENT_GATEWAY_URLwas not set, so auth-aware proxy tools were not activatedKEYCLOAK_SUPERVISOR_CLIENT_SECRETwas not mapped, so OBO exchange could not workMCPAuthMiddlewarevalidated auth but discardedUserContext- MCP tool functions called
vector_db_query_service.query()with no team filtering
MCP RBAC Data Flow (After Fix)β
ββββββββββββ JWT ββββββββββββββββ OBO Token βββββββββββββββββ
β Slack β βββββββ β Supervisor β ββββββββββββ β Agent Gateway β
β User β β (caipe-sup) β β (AG) β
ββββββββββββ ββββββββ¬ββββββββ ββββββββ¬βββββββββ
β β
wraps tools via validates JWT,
auth_mcp_tools.py applies CEL,
(OBO exchange) proxies to RAG
β β
ββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββ
β RAG Server (/mcp) β
β β
β MCPAuthMiddleware β
β ββ validate Bearer JWT β
β ββ build UserContext β
β ββ set contextvars β
β β
β MCP Tool (search, etc.) β
β ββ read UserContext β
β ββ extract team_id β
β ββ get_accessible_kb_idsβ
β ββ filter query results β
βββββββββββββββββββββββββββββ
Changes Madeβ
docker-compose.dev.yamlβ
- Set
AGENT_GATEWAY_URLdefault tohttp://agentgateway:4000for supervisor and dynamic-agents - Map
KEYCLOAK_SUPERVISOR_CLIENT_IDandKEYCLOAK_SUPERVISOR_CLIENT_SECRETfor OBO exchange
ai_platform_engineering/knowledge_bases/rag/server/src/server/restapi.pyβ
- Added
mcp_user_context_var: ContextVar[Optional[UserContext]] - Modified
MCPAuthMiddleware.dispatch()to storeUserContextonrequest.state.userand set the context variable for both JWT-authenticated and trusted-network requests - Context variable is properly reset after each request using
try/finally
ai_platform_engineering/knowledge_bases/rag/server/src/server/tools.pyβ
- Added
_get_mcp_user_context(): readsUserContextfrommcp_user_context_var - Added
_extract_team_id(): parsesteam_member(<id>)from realm roles - Added
_resolve_accessible_kb_ids(): resolves accessible KB IDs viaget_accessible_kb_ids(), returns None for unrestricted access - Modified
search(): applies datasource_id filter before querying - Modified
fetch_document(): adds datasource_id filter to document fetch - Modified
list_datasources_and_entity_types(): filters returned datasource list - Modified
_make_search_fn/_execute(): intersects per-search datasource_ids with RBAC-accessible IDs
FR-039: AG Dynamic CEL Policy Management Architectureβ
Overviewβ
Agent Gateway reads CEL authorization rules from config.yaml (file-watched for hot-reload). The Admin UI stores policies in MongoDB. A config-bridge sidecar synchronizes policies from MongoDB to AG's config file, enabling zero-downtime policy updates from the Admin UI.
Component Architectureβ
Hot-Reload Flowβ
CEL Validation Strategyβ
Two-layer validation prevents invalid policies from reaching Agent Gateway:
-
Client-side (live): The
AgMcpPoliciesEditorcomponent usescel-js(via@/lib/rbac/cel-evaluator) to validate expressions as the admin types (debounced 300ms). A mock AG context withjwt.realm_access.roles,mcp.tool.name, andrequest.headersis used for dry-run evaluation, showing whether the expression would allow or deny the mock request. -
Server-side (on save): The BFF route (
/api/rbac/ag-policiesPUT) runsevalCel(expression, agDryContext)before upserting to MongoDB. Invalid expressions return HTTP 400 with the parse error.
MongoDB Collectionsβ
| Collection | Purpose | Key Fields |
|---|---|---|
ag_mcp_policies | CEL rules per backend/tool pattern | backend_id, tool_pattern, expression, enabled |
ag_mcp_backends | MCP upstream targets | id, upstream_url, description, enabled |
ag_sync_state | Generation counter for sync tracking | policy_generation, bridge_generation, bridge_last_sync, bridge_error |
Deployment Modelsβ
Docker dev (docker-compose.dev.yaml):
ag-config-bridgecontainer sharesag_confignamed volume withagentgateway- Bridge writes to
/etc/agentgateway/config.yaml; AG reads from the same path - Bridge polls MongoDB every 5 seconds (configurable via
AG_POLL_INTERVAL)
Kubernetes prod (future):
- Option A: Sidecar in AG pod +
emptyDirshared volume - Option B: If kgateway is adopted, migrate to
AgentgatewayPolicyCRDs via K8s operator
Files Changedβ
| File | Change |
|---|---|
deploy/agentgateway/config.yaml.j2 | New β Jinja2 template for AG config |
deploy/agentgateway/config-bridge.py | New β Python sidecar with MongoDB poll + template render |
deploy/agentgateway/Dockerfile.config-bridge | New β Container image for bridge |
deploy/agentgateway/requirements.txt | New β pymongo + jinja2 |
ui/src/app/api/rbac/ag-policies/route.ts | New β BFF CRUD with CEL validation |
ui/src/app/api/rbac/ag-sync-status/route.ts | New β Sync status endpoint |
ui/src/components/admin/AgMcpPoliciesEditor.tsx | New β Admin UI editor with validation + hot-reload |
ui/src/lib/rbac/types.ts | Added AgMcpPolicy, AgMcpBackend, AgSyncState types |
ui/src/lib/mongodb.ts | Added indexes for new collections |
ui/src/app/api/rbac/admin-tab-gates/route.ts | Added ag_policies tab gate |
ui/src/app/(app)/admin/page.tsx | Added AG MCP Policies tab to Security & Policy category |
docker-compose.dev.yaml | Added ag-config-bridge service + ag_config shared volume |
Related Documentsβ
- spec.md β Feature specification (098)
- 093 architecture β Historical architecture (superseded)
- 093 research index β Policy engine comparison, AG/Keycloak research
- Agent Gateway β Upstream project
- AG Keycloak tutorial β OIDC integration reference