Skip to main content
Version: main 🚧

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​

StepFromToDescription
β‘ UserSlack / Webex / Admin UIUser sends command or performs admin action
β‘‘Entry pointBot Backend / BFFEvent delivered with user context (Slack signature, OAuth, NextAuth session)
β‘’aEnterprise IdPKeycloakFederation: Okta/Entra groups and identity brokered into Keycloak
β‘’bBot/BFFKeycloakOBO token exchange (RFC 8693): bot obtains JWT with sub=user, act=bot, groups, roles, scope, org
β‘£aBot/BFFKeycloak AuthZ (PDP)UI/Slack authorization: checks JWT + requested capability against 098 matrix β†’ allow/deny
β‘£bBot/BFFAgent GatewayMCP/A2A/agent authorization: AG validates JWT, applies CEL policy β†’ allow/deny
β‘€aAgent GatewaySupervisor β†’ AgentsAuthorized request routed to domain agents
β‘€bAgentsAgent Gateway β†’ MCPAgent MCP tool calls re-enter AG for tool-level RBAC (FR-016)
β‘₯PDP / RAGMongoDB + KeycloakHybrid store: Keycloak holds authz policies (resources, scopes, permissions) and Slack identity links (user attributes); MongoDB holds team/KB assignments, ASP policies, app metadata
⑦AgentsGitHub / Jira / ArgoCDDownstream API calls using brokered user tokens

Authorization Enforcement Points​

098 defines three enforcement zones, each with its own PDP:

ZoneEnforcement PointPDPTraffic
UINext.js BFF (NextAuth middleware)Keycloak Authorization ServicesAdmin UI API routes, page access
Slack / WebexBot backend middlewareKeycloak Authorization ServicesSlack commands, Webex events
MCP / A2A / AgentAgent 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:

LayerMapper TypeKeycloak ConfigPurpose
1. ImportIdentity Provider MapperSAML: "Attribute Importer" β€” attribute name groups β†’ user attribute idp_groupsExtracts groups from IdP assertion/token into Keycloak user profile
OIDC: "Claim to User Attribute" β€” claim groups β†’ user attribute idp_groups
2. Map to RolesIdentity Provider Mapper"Hardcoded Role" or "SAML Attribute IdP Role Mapper" β€” when groups contains platform-admin β†’ assign KC realm role adminConverts IdP group membership into Keycloak realm roles
Repeat per group β†’ role mapping
3. Emit in JWTClient Protocol Mapper"Group Membership" β†’ token claim groupsEmits groups in JWT for downstream consumers
"Realm Role" β†’ token claim rolesEmits mapped roles in JWT
"User Attribute" β†’ token claim orgEmits tenant/org context in JWT

IdP-Specific Federation Setup​

IdPProtocolGroup SourceKeycloak Broker Config
Okta (SAML)SAML 2.0SAML Assertion β†’ Attribute Statement groupsIdentity Provider β†’ SAML β†’ Import SAML attributes
Okta (OIDC)OIDCID Token β†’ groups claim (requires Okta "Groups claim" config in the Okta app)Identity Provider β†’ OIDC β†’ Import OIDC claims
Microsoft Entra ID (SAML)SAML 2.0SAML Assertion β†’ http://schemas.microsoft.com/ws/2008/06/identity/claims/groups (GUIDs) or custom groups attributeIdentity Provider β†’ SAML β†’ Attribute Importer (map GUIDs or group names)
Microsoft Entra ID (OIDC)OIDCID 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_displayName source attribute in the enterprise app's SAML claims configuration.

Group β†’ Role Mapping Table​

IdP GroupKeycloak Realm RoleCapabilities (098 matrix examples)
platform-adminadminAll protected capabilities
team-a-engteam_member(team-a)Chat, invoke team-a tools, query team-a KBs
kb-adminskb_adminCreate/update/delete KBs, manage ingest
team-b-opsteam_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​

AspectDetail
WhereKeycloak 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 dependenciesKeycloak 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.

PathPDPHow
UI / Slack / WebexKeycloak Authorization ServicesUMA / resource-based permissions; 098 matrix modeled as KC resources, scopes, policies
MCP / A2A / AgentAgent GatewayCEL policy; JWT issued by Keycloak

Keycloak Authorization Services:

  • Consume JWT groups, roles, scope, org claims (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 RoleRAG Server RolePermissionsKB Access
adminadminread, ingest, deleteAll KBs (global override)
kb_adminingestonlyread, ingestAll KBs (global override)
team_memberreadonlyreadTeam-owned KBs only
chat_userreadonlyreadPer-KB roles or team-owned KBs
kb_reader:<kb-id>readonly (scoped)readSpecified KB only
kb_reader:*readonly (all)readAll KBs (wildcard)
kb_ingestor:<kb-id>ingestonly (scoped)read, ingestSpecified KB only
kb_ingestor:*ingestonly (all)read, ingestAll KBs (wildcard)
kb_admin:<kb-id>admin (scoped)read, ingest, deleteSpecified 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:

LayerCheckPDPScopeFailure Mode
1. BFF /api/rag/kb/*requireRbacPermission("rag", "kb.query")Keycloak AuthZCoarse capability (can user do KB operations at all?)401/403 to UI
2. RAG global rolerequire_role(Role.READONLY) via JWT β†’ Keycloak role mapperRAG server (JWT)Global role check (is user authenticated with sufficient role?)401/403 from RAG
3. RAG per-KB accessrequire_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 filterinject_kb_filter() in /v1/queryRAG 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 RoleScopes GrantedAgent Access
adminview, invoke, configure, deleteAll agents (global override)
agent_admin:<agent-id>view, invoke, configure, deleteSpecified agent only
agent_admin:*view, invoke, configure, deleteAll agents (wildcard)
agent_user:<agent-id>view, invokeSpecified agent only
agent_user:*view, invokeAll agents (wildcard)
team_member(team-x)view, invoke (team agents)Agents with visibility: team + shared_with_teams includes team-x
(owner)view, invoke, configure, deleteOwn 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​

ComponentRoleRequired?Authorization
Slack / WebexUser-facing entry (at least one)At least one channelBot validates identity + PDP check
CAIPE Admin UIAdmin web interfaceYesNextAuth session + PDP check
Slack Bot / Webex Bot BackendEvent handling, identity resolution (via Keycloak user attributes), OBO exchangeYesPDP 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)RequiredSource of JWT claims + PDP for UI/Slack + identity link store
Enterprise IdPSSO (Okta SAML, Entra AD); federated into KeycloakOptionalFederation source
Agent GatewayMCP/A2A gateway, JWT validation, CEL policyRequiredPDP for agent traffic
Supervisor / OrchestratorA2A server, agent routingYesCarries forwarded identity
Domain AgentsGitHub, Jira, ArgoCD, etc.YesOBO tokens for downstream
MCP ServersTool invocationYesAG-gated access
RAG ServerKBs, datasourcesYesPDP-gated admin; AG-gated queries; CEL per-KB access (FR-027)
Dynamic AgentsUser-created/runtime agents, deepagent LangGraphYesThree-layer RBAC: Keycloak resource + per-agent roles + MongoDB visibility + CEL (FR-028); MCP calls via AG (FR-030)
Slack BotSlack commands, identity linking, channel-team scopingYesIdentity 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)
MongoDBUsers, policies, permission matrix, team/KB config (no Slack identity links β€” those are in Keycloak)YesData store for PDP + Admin UI

Fail-Closed Behavior​

FailureImpactBehavior
Agent Gateway downMCP/A2A/agent trafficDenied (fail closed). Slack and Admin UI unaffected.
Keycloak downToken issuance, login, UI/Slack authzNo new sessions; existing valid JWTs may continue until expiry; authz checks denied (fail closed).
MongoDB unavailableMatrix/config readsPDP 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​

  1. User sends chat β†’ UI attaches accessToken as Bearer header
  2. BFF forwards Bearer to supervisor A2A endpoint
  3. 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 with sub=user, act.sub=caipe-platform
  4. Auth-aware proxy tool is invoked by LangGraph; it reads obo_token from RunnableConfig.configurable, creates an ephemeral MCP client with Authorization: Bearer <obo_token>, connects to AG
  5. Agent Gateway validates the OBO JWT via Keycloak JWKS, evaluates CEL tool-level policies, forwards the MCP request to RAG server target
  6. RAG server extracts team membership from JWT roles (team_member(<id>)), queries team_kb_ownership in MongoDB for allowed datasource IDs, filters results, returns only team-authorized data

Data Flow: Team-Scoped KB Ingest via UI​

  1. User navigates to KB β†’ IngestView; UI calls GET /api/rag/kb/v1/datasources
  2. BFF proxy adds X-Team-Id header (from session JWT roles) and proxies to RAG server
  3. RAG server checks team_kb_ownership β€” if user's team has ingest or admin on the target datasource, allow; otherwise deny
  4. UI hides ingest/delete buttons for KBs where the user's team lacks matching permissions; shows team-ownership badges

Fallback Behavior​

ConditionBehavior
AGENT_GATEWAY_URL unsetSupervisor + dynamic agents connect directly to MCP servers (no AG, no OBO)
OBO exchange failsSupervisor falls back to service-account token (reduced access)
MongoDB team_kb_ownership unreachableRAG server returns empty results (fail-closed)
AG downMCP 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​

FilePurpose
ui/src/components/rag/KbTeamAccessPanel.tsxReusable panel (compact popover + full inline) for managing team-KB assignments per datasource

Modified Files​

FileChanges
ui/src/components/rag/IngestView.tsxImport 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.tsAlready 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:

  1. AGENT_GATEWAY_URL was not set, so auth-aware proxy tools were not activated
  2. KEYCLOAK_SUPERVISOR_CLIENT_SECRET was not mapped, so OBO exchange could not work
  3. MCPAuthMiddleware validated auth but discarded UserContext
  4. 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_URL default to http://agentgateway:4000 for supervisor and dynamic-agents
  • Map KEYCLOAK_SUPERVISOR_CLIENT_ID and KEYCLOAK_SUPERVISOR_CLIENT_SECRET for 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 store UserContext on request.state.user and 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(): reads UserContext from mcp_user_context_var
  • Added _extract_team_id(): parses team_member(<id>) from realm roles
  • Added _resolve_accessible_kb_ids(): resolves accessible KB IDs via get_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:

  1. Client-side (live): The AgMcpPoliciesEditor component uses cel-js (via @/lib/rbac/cel-evaluator) to validate expressions as the admin types (debounced 300ms). A mock AG context with jwt.realm_access.roles, mcp.tool.name, and request.headers is used for dry-run evaluation, showing whether the expression would allow or deny the mock request.

  2. Server-side (on save): The BFF route (/api/rbac/ag-policies PUT) runs evalCel(expression, agDryContext) before upserting to MongoDB. Invalid expressions return HTTP 400 with the parse error.

MongoDB Collections​

CollectionPurposeKey Fields
ag_mcp_policiesCEL rules per backend/tool patternbackend_id, tool_pattern, expression, enabled
ag_mcp_backendsMCP upstream targetsid, upstream_url, description, enabled
ag_sync_stateGeneration counter for sync trackingpolicy_generation, bridge_generation, bridge_last_sync, bridge_error

Deployment Models​

Docker dev (docker-compose.dev.yaml):

  • ag-config-bridge container shares ag_config named volume with agentgateway
  • 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 + emptyDir shared volume
  • Option B: If kgateway is adopted, migrate to AgentgatewayPolicy CRDs via K8s operator

Files Changed​

FileChange
deploy/agentgateway/config.yaml.j2New β€” Jinja2 template for AG config
deploy/agentgateway/config-bridge.pyNew β€” Python sidecar with MongoDB poll + template render
deploy/agentgateway/Dockerfile.config-bridgeNew β€” Container image for bridge
deploy/agentgateway/requirements.txtNew β€” pymongo + jinja2
ui/src/app/api/rbac/ag-policies/route.tsNew β€” BFF CRUD with CEL validation
ui/src/app/api/rbac/ag-sync-status/route.tsNew β€” Sync status endpoint
ui/src/components/admin/AgMcpPoliciesEditor.tsxNew β€” Admin UI editor with validation + hot-reload
ui/src/lib/rbac/types.tsAdded AgMcpPolicy, AgMcpBackend, AgSyncState types
ui/src/lib/mongodb.tsAdded indexes for new collections
ui/src/app/api/rbac/admin-tab-gates/route.tsAdded ag_policies tab gate
ui/src/app/(app)/admin/page.tsxAdded AG MCP Policies tab to Security & Policy category
docker-compose.dev.yamlAdded ag-config-bridge service + ag_config shared volume