Tasks: Comprehensive RBAC Tests + Completion of 098
Input: Design documents from docs/docs/specs/102-comprehensive-rbac-tests-and-completion/
Prerequisites: spec.md, plan.md, call-sequences.md, research.md, data-model.md, quickstart.md, contracts/python-rbac-helper.md
Tests: REQUIRED throughout. The whole point of this spec is comprehensive automated tests; every implementation task ships with the matrix entry and parameterised test that exercise it. Test tasks are NOT optional here.
Organisation: Tasks are grouped by user story (US1βUS8 from spec.md) so each story can be implemented, tested, and shipped independently. Phase order follows the plan's risk-ascending sequence: simpler/lower-risk migrations first (US1), test scaffolding next (US7) so subsequent phases get tests for free, then the bigger Python migrations (US3 β US2 β US4 β US6), then the higher-risk Slack OBO and the doc rewrite (US5 + US8).
Format: [ID] [P?] [Story] Descriptionβ
- [P]: Can run in parallel (different files, no dependencies on incomplete tasks).
- [Story]: Which user story this task belongs to (US1, US2, β¦ US8). Setup, Foundational, and Polish tasks have no story label.
- File paths are absolute relative to the repo root.
Path Conventionsβ
- Backend (Python):
ai_platform_engineering/ - Frontend (TypeScript / Next.js BFF):
ui/ - Tests (cross-cutting RBAC):
tests/rbac/(NEW perresearch.mdΒ§2) - Realm config:
deploy/keycloak/ - Compose stacks:
docker-compose/ - Automation scripts:
scripts/
Phase 1: Setup (Shared Infrastructure)β
Purpose: Repo-level scaffolding that every later phase consumes. Maps to plan.md Β§Phase 0 β Research, Schema, Fixtures.
- T001 Create top-level
tests/rbac/directory structure:tests/rbac/{fixtures,unit/ts,unit/py,e2e}/with empty__init__.py/index.tsplaceholders so each component's runner can import from it - T002 [P] Add a
pyproject.toml(or extend the root one) entry to registertests/rbac/conftest.pysoPYTHONPATH=. uv run pytest tests/rbac/discovers the suite from repo root - T003 [P] Add
tests/rbac/.gitkeepand a top-leveltests/rbac/README.mdlinking todocs/docs/specs/102-comprehensive-rbac-tests-and-completion/quickstart.md - T004 [P] Create
docker-compose/docker-compose.e2e.override.yamlas a thin overlay on top ofdocker-compose.dev.yaml(per spec Clarification 2026-04-22). The overlay only remaps host ports to avoid colliding with a running dev stack (e.g. mongo27017β27018, UI3000β3010, supervisor8000β8010perquickstart.md) and injects e2e-only env vars where needed. NO duplication of the full service definitions β every service is sourced from the dev compose file viaCOMPOSE_PROFILES - T005 Modify
deploy/keycloak/docker-compose.ymlto enable--features=token-exchange,admin-fine-grained-authzon the Keycloak container (resolves Open Question 4 βresearch.mdΒ§4) - T006 Add a stub
make test-rbactarget toMakefilethat runsbash -c "echo 'test-rbac wired in T037'; exit 0". Real implementation lands in T037; this exists so phase-1 acceptance ("make test-rbacexists") passes - T007 [P] Add
make test-rbac-jest,make test-rbac-pytest,make test-rbac-e2estub targets toMakefile(real bodies wired by T038/T039/T040)
Checkpoint: Setup ready β directory exists, compose file exists, make targets exist (vacuous).
Phase 2: Foundational (Blocking Prerequisites)β
Purpose: The matrix, schemas, persona fixture, Python helpers, and linters that every user-story phase consumes. Until this completes, no user-story phase can produce green tests.
β οΈ CRITICAL: User-story phases MUST NOT begin until this phase is checkpointed.
Matrix + lintersβ
- T008 Create the empty matrix file
tests/rbac/rbac-matrix.yamlwithversion: 1androutes: []; add an inline comment pointing todocs/docs/specs/102-comprehensive-rbac-tests-and-completion/contracts/rbac-matrix.schema.json - T009 [P] Implement
scripts/validate-rbac-matrix.py: walksui/src/app/api/{admin,dynamic-agents,mcp-servers,teams,agents}/**/route.tsandai_platform_engineering/**/server.py, fails if anyrequireRbacPermission/require_rbac_permissioncall is not represented intests/rbac/rbac-matrix.yaml; prints actionable diff (per FR-010, SC-006) - T010 [P] Implement
scripts/extract-rbac-resources.py: greps for every(resource, scope)literal pair in TS+Py code perdata-model.mdΒ§E4; emitsui/src/lib/rbac/resource-catalog.generated.ts(KEYCLOAK_RESOURCE_CATALOGconst) - T011 [P] Implement
scripts/validate-realm-config.py: loadsKEYCLOAK_RESOURCE_CATALOG, asserts every(resource, scope)exists indeploy/keycloak/realm-config.jsonauthorizationSettings.resources[].scopes(per FR-006). Hard-gate, exit non-zero on drift - T012 Add a unit test for the matrix linter at
tests/rbac/unit/py/test_validate_matrix_lint.py: feeds the linter a fixture route file and a fixture matrix; asserts pass on match, fail on missing entry, fail on invalid persona, fail on(resource, scope)not in realm config
Realm config extras (PDP-unavailable fallback)β
- T013 Create
deploy/keycloak/realm-config-extras.jsonwith{ "version": 1, "pdp_unavailable_fallback": { "admin_ui": { "mode": "realm_role", "role": "admin" } } }β preserves existing behaviour as research.md Β§1 mandates - T014 [P] Add JSON-schema validation in
scripts/validate-realm-config.py(T011) that assertsrealm-config-extras.jsonvalidates againstdocs/docs/specs/102-comprehensive-rbac-tests-and-completion/contracts/realm-config-extras.schema.json
Persona fixture (TS + Py)β
- T015 Implement
tests/rbac/fixtures/keycloak.pywithget_persona_token(name)andclear_persona_cache()perdata-model.mdΒ§E5; uses Resource Owner Password Credentials grant againsthttp://localhost:7080/realms/caipe/protocol/openid-connect/token; in-memory token cache with refresh-30s-before-expiry - T016 [P] Implement
tests/rbac/fixtures/keycloak.tswithgetPersonaToken+clearPersonaCacheperdata-model.mdΒ§E5; identical behaviour to T015 (parity is asserted by T031) - T017 Create
tests/rbac/conftest.py: pytest fixturesalice_admin,bob_chat_user,carol_kb_ingestor,dave_no_role,eve_dynamic_agent_user,frank_service_accountreturningPersonaToken; plus@pytest.fixture(params=PERSONAS)for matrix-driven tests - T018 Create
tests/rbac/fixtures/audit.pyandtests/rbac/fixtures/audit.ts:assert_audit_record(user_id, resource, scope, allowed, reason)helpers reading from MongoDBauthz_decisions; used by every matrix-driven test - T019 Modify
deploy/keycloak/init-idp.sh(existing script) to seed the six personas with the realm/client roles, team memberships, and per-KB roles defined inspec.mdΒ§Personas. Idempotent (re-runnable). Verifykcadm.sh get users -r caipeshows all six after compose-up
Python RBAC helpers (mirrors of TS implementation)β
- T020 Implement
ai_platform_engineering/utils/auth/jwks_validate.py(FR-002):validate_bearer_jwt(token) -> dictβ JWKS fetch + caching (TTL β₯ 5 min), RS256 signature verify,iss/exp/audchecks, raises on invalid; usespython-joseperresearch.mdΒ§TD-2 - T021 Implement
ai_platform_engineering/utils/auth/keycloak_authz.py(FR-003):require_rbac_permission(token, resource, scope) -> AuthzDecisionpercontracts/python-rbac-helper.md; includes_CACHE: TTLCache,_cache_key, fallback consult ofrealm-config-extras.json - T022 [P] Implement
ai_platform_engineering/utils/auth/audit.py(FR-007):log_authz_decision(decision_dict)writes to MongoDBauthz_decisionscollection (best-effort, swallow + WARN on failure); document shape validates againstcontracts/audit-event.schema.json - T023 [P] Implement
ai_platform_engineering/utils/auth/realm_extras.py:get_fallback_rule(resource) -> dict | Nonereadsrealm-config-extras.json(path fromRBAC_FALLBACK_CONFIG_PATHenv, default/etc/keycloak/realm-config-extras.json) - T024 Add
require_rbac_permission_dep(resource, scope)FastAPI dependency factory inai_platform_engineering/utils/auth/keycloak_authz.pypercontracts/python-rbac-helper.md; raisesHTTPException(403)on deny so handlers stay clean
Tests for foundational helpers (TDD β write & confirm RED before T020/T021/T022 implementations)β
- T025 [P] Write
tests/rbac/unit/py/test_jwks_validate.pycovering: valid token (mock JWKS), expired token, wrong issuer, wrong audience, signature mismatch. Confirm RED before T020 implementation - T026 [P] Write
tests/rbac/unit/py/test_keycloak_authz.pycovering eachAuthzReasonpath (cache hit/miss, PDP allow, PDP 403, PDP unreachable + fallback rule present, PDP unreachable + no rule). Confirm RED before T021 - T027 [P] Write
tests/rbac/unit/py/test_audit.pycovering: successful Mongo write, Mongo write failure does NOT raise, document validates againstaudit-event.schema.json. Confirm RED before T022 - T028 [P] Write
tests/rbac/unit/py/test_realm_extras.pycovering: file present + valid, file present + malformed, file missing (returns None), unknown resource (returns None). Confirm RED before T023
TSβPy parity testβ
- T029 Add
(resource, scope)cross-references to JSON schemas indocs/docs/specs/102-comprehensive-rbac-tests-and-completion/contracts/(already drafted) β verify each contract references its peers - T030 Implement
tests/rbac/unit/py/test_helper_parity.py: parameterised over personas; assertsawait require_rbac_permission(token, R, S).allowed == ts checkPermission(token, R, S).allowedfor every (R, S) defined in matrix (parity invariants 1+2 fromcontracts/python-rbac-helper.md) - T031 [P] Implement
tests/rbac/fixtures/test_fixture_parity.pysmoke test: get a persona token via the Py fixture and again via the TS fixture (subprocess shelling out tonode -e); assert the decodedsubclaim is identical
make test-rbac real wiring (replaces T006 stub)β
- T032 Replace stub in
Makefile:make test-rbacnow runs in order:make test-rbac-lint(scripts/validate-rbac-matrix.py+scripts/validate-realm-config.py) βmake test-rbac-pytest(helper unit + parity tests) βmake test-rbac-jest(UI matrix-driver) βmake test-rbac-e2e(Playwright; gated onRBAC_E2E=1).test-rbac-upbrings upCOMPOSE_PROFILES="rbac,caipe-ui,caipe-supervisor,caipe-mongodb,dynamic-agents,rag,all-agents,slack-bot" docker compose -f docker-compose.dev.yaml -f docker-compose/docker-compose.e2e.override.yaml up -d --waitand seeds personas viadeploy/keycloak/init-idp.sh;test-rbac-downtears it down. Strict matrix lint viaRBAC_LINT_STRICT=1(allowed to soft-fail during phase rollout per spec.md Clarification). Honors FR-009. Per spec Clarification 2026-04-22: NO separatedocker-compose.e2e.yamlβ reuse the dev compose file with profiles + a thin overlay
Checkpoint: Foundation ready β matrix + helpers + fixtures + linters all in place. All eight user stories below can now begin in parallel. Until this checkpoint, the matrix-driven tests in every later phase will trivially fail/skip.
Phase 3: User Story 1 β Admin UI is fully Keycloak-gated (Priority: P1) π― MVPβ
Goal: Every BFF route under /api/admin/* (and /api/dynamic-agents/*, /api/mcp-servers/*, /api/teams/*, /api/agents/*) gates on Keycloak via requireRbacPermission. Legacy requireAdmin / requireAdminView remain only for /api/internal/* (out of scope) and are flagged unused on production paths.
Independent Test: Boot Keycloak + UI via make test-rbac-up (which sets COMPOSE_PROFILES="rbac,caipe-ui,caipe-supervisor,caipe-mongodb,..." against docker-compose.dev.yaml plus the e2e override). Log in as each persona. Hit every route in the matrix under those prefixes. Assert 200/403 per matrix entry. Verify authz_decisions Mongo collection has one document per call.
Maps to: Story 1, FR-001, FR-006, FR-010 (subset).
Realm config seeding for US1β
- T033 [US1] Add resources to
deploy/keycloak/realm-config.jsonper FR-006:admin_ui(existing β scopesview,configure,admin,audit.view),team(NEW β scopesview,manage),mcp_server(NEW β scopesread,manage); add policiesteam-view-access/team-manage-access/mcp-server-read-access/mcp-server-manage-accessbindingadminrealm role tomanageandchat_user/team_memberto read scopes. Also extendedui/src/lib/rbac/types.tsto add the newRbacResourceandRbacScope(read/manage) literals, andui/src/lib/api-middleware.tsRESOURCE_ROLE_FALLBACKto mapteamβadmin,mcp_serverβadmin - T034 [P] [US1] Add resources to
deploy/keycloak/realm-config.json:dynamic_agent(NEW β scopesview,invoke,manage); added policiesdynamic-agent-invoke-access(bindschat_usertoview+invoke) anddynamic-agent-manage-access(bindsadmintoview+invoke+manage). Per-agentdynamic_agent:<id>resources still seeded later by US6's runtime (Phase 8). Fallback:dynamic_agentβchat_user
Matrix entries for US1β
- T035 [US1] Added matrix entries for all 49 admin routes under
ui/src/app/api/admin/**/route.tsper FR-001 viascripts/generate-rbac-matrix-us1.py. Mapping:(admin_ui, view)for read-only routes;(admin_ui, admin)for mutating;(admin_ui, audit.view)for audit routes. The 11 routes under/api/admin/teams/**map to(team, view)/(team, manage)per FR-006. All 6 personas covered. Pre-migration routes are taggedmigration_status: pendingso the matrix-driver renders them asxit(yellow/pending) β the linter still requires the entry, only test execution is gated. Phase 11 (T127) verifies nopendingrows remain - T036 [P] [US1] Added matrix entries for the 23 routes under
ui/src/app/api/{dynamic-agents,mcp-servers,agents}/**/route.tsvia the same generator. Mapping:dynamic-agentsβdynamic_agent/view|invoke|manage;mcp-serversβmcp_server/read|manage;agents/toolsβmcp_server/read./api/dynamic-agents/healthand/api/dynamic-agents/builtin-toolsare intentionally excluded (unauthenticated infra endpoints)./api/teamsdirectory does not exist; the team routes live under/api/admin/teams/**. Also extendedrbac-matrix.schema.jsonwithmigration_status: enum(migrated|pending)to preserve immutability of the contract - T037 [US1] Initial GREEN matrix-linter run after T035/T036: 73 generated routes + 1 hand-curated supervisor#invoke entry + 1 smoke entry. Linter PASS β every
requireRbacPermission(...)call site inui/src/app/api/**/route.tsis now represented. Pre-migration handlers (those still callingrequireAdmin) appear in the matrix asmigration_status: pending; the matrix-driver renders 402 tests asxit(pending). Subsequent T040βT049 migration tasks will flip each tomigration_status: migratedand the driver assertions will activate - T038 [US1] Implement Jest matrix-driver at
ui/src/__tests__/rbac-matrix-driver.test.ts. Loadstests/rbac/rbac-matrix.yaml, filters tosurface: ui_bff, mocksgetServerSessionper persona +checkPermissionper matrix expectation, then dynamically imports eachroute.tsand dispatches the declared method against aNextRequest. Asserts[401,403]for deny, otherwise non-401/403 for allow. Pending-aware: rows withmigration_status: pendingrender asxit()(yellow); migrated rows run the assertion. With T035βT037 populated: 450 total tests, 48 passing (smoke + chat/conversations + admin/teams reads), 402 pending (waiting on T040βT049). Empty-matrix branch (Phase 2) and pending-skip branch (Phase 3 rollout) both render a clean test summary
Jest matrix-driven test driver (TDD-RED for US1)β
- T038 [US1] Implement
tests/rbac/unit/ts/matrix-driver.test.ts: loadstests/rbac/rbac-matrix.yaml, filters tosurface: ui_bffentries, parameterises over each (route Γ persona), uses Next.js test helpers + the persona fixture (T016) to issue real HTTP-equivalent calls into the route handler, asserts status + reason + audit record (via T018). Wiremake test-rbac-jestto point at this file - T039 [US1] Add
tests/rbac/unit/ts/__snapshots__/to.gitignoreif not already present (we never want jest snapshot drift to mask RBAC regressions)
Migrate admin routes (mechanical swap; group commits per cluster)β
- T040 [US1] Migrate
ui/src/app/api/admin/users/**/route.ts(5 files:route.ts,[id]/route.ts,[id]/role/route.ts,[id]/roles/route.ts,[id]/teams/route.ts): replacerequireAdmin(session)withawait requireRbacPermission(session, 'admin_ui', '<view|manage>'). Pick scope per HTTP method (GET βview, all others βmanage) - T041 [P] [US1] Migrate
ui/src/app/api/admin/teams/**/route.ts(4 files): same pattern as T040 but with(resource: 'team', scope: 'view'|'manage') - T042 [P] [US1] Migrate
ui/src/app/api/admin/roles/**/route.ts(2 files),role-mappings/**/route.ts(2 files):(admin_ui, manage)for all (these are mutation-heavy) - T043 [P] [US1] Migrate
ui/src/app/api/admin/slack/**/route.ts(3 files):(admin_ui, view)for GETs,(admin_ui, manage)for mutations - T044 [P] [US1] Migrate
ui/src/app/api/admin/audit-{events,logs,logs/[id]/messages,logs/owners,logs/export}/route.ts(5 files):(admin_ui, view)for all (read-only) - T045 [P] [US1] Migrate
ui/src/app/api/admin/{nps,nps/campaigns,feedback,metrics,migrate-conversations,rbac-audit}/route.ts(6 files):(admin_ui, view)for GETs,(admin_ui, manage)for POSTs - T046 [P] [US1] Migrate
ui/src/app/api/admin/stats/**/route.ts(3 files includingroute.ts,skills/route.ts,checkpoints/route.ts):(admin_ui, view)for all - T047 [P] [US1] Migrate
ui/src/app/api/dynamic-agents/**/route.ts(10 files): map per FR-001; chat-stream-start gets per-agent(dynamic_agent:<agent_id>, invoke)(this overlaps with US6 Phase 8 β coordinate by leavingchat/stream/startunmigrated here and reverting to it in T087) - T048 [P] [US1] Migrate
ui/src/app/api/mcp-servers/**/route.ts(2 files):(mcp_server, read|manage) - T049 [P] [US1] Migrate
ui/src/app/api/agents/tools/route.ts:(mcp_server, read)
Mark legacy gates deprecated for productionβ
- T050 [US1] Modify
ui/src/lib/api-middleware.ts: add a@deprecatedJSDoc comment onrequireAdminandrequireAdminViewsaying "UserequireRbacPermission(session, '<resource>', '<scope>')instead. Production callers MUST be removed; any new use will failscripts/validate-rbac-matrix.py." - T051 [US1] Add a CI assertion in
scripts/check-no-new-requireAdmin.sh(wired viamake test-rbac-lint): everyroute.tsthat importsrequireAdmin/requireAdminViewfrom@/lib/api-middlewaremust have a matchingmigration_status: pendingentry intests/rbac/rbac-matrix.yaml; new call sites outside that allowlist hard-fail whenRBAC_LINT_STRICT=1. Stale legacyrequireAdmin(session)calls were removed from the two now-migrated routes (POST /api/admin/teams,PATCH /api/admin/users/[id]/role); 13 remaining call sites outside Phase 3 scope (catalog/skills/policies/llm-models) were added to the matrix aspendingviascripts/append-pending-rbac-entries.pyso the guard reports a clean state today (50 pending entries cover all 32 import sites).
Existing test fixes (handle pre-existing test breakage caused by migrations)β
- T052 [US1] Updated
ui/src/app/api/__tests__/admin-feedback.test.tsso it mockscheckPermissionfrom@/lib/rbac/keycloak-authz(andlogAuthzDecision) the same wayadmin-stats.test.tsandadmin-users-stats.test.tsalready did. The route itself was migrated in this task βui/src/app/api/admin/feedback/route.tspreviously gated only onwithAuth, now requiresrequireRbacPermission(session, 'admin_ui', 'view')(FR-001). The misleading "returns 200 for any authenticated user (no admin gate on route)" assertion was rewritten to "returns 403 for non-admin users (admin_ui#view denied)". The matrix entry forGET /api/admin/feedbackwas flipped frompendingβmigrated. The other two test files needed no changes.
Acceptance check for US1β
- T053 [US1] Ran the assembly:
make test-rbac-lintis green (matrix linter + realm-config validator + requireAdmin deprecation guard), andmake test-rbac-jestruns 3 suites with 72 assertions passing, 0 failing, 474 skipped (the skipped rows are themigration_status: pendingroutes covered by Phases 5β9 β they're already in the matrix but their handlers aren't migrated yet). The deprecation-guard report shows 32 legacyrequireAdminimport sites, all covered by 49 pending matrix entries. Phase 3 acceptance criterion (FR-001 + SC-001) is satisfied for every Phase-3-scoped route; remainingpendingrows are tracked for Phases 5β9.
Checkpoint: User Story 1 fully functional and independently testable. Admin UI is the first surface that can be demoed as Keycloak-only β even if every later phase slips, this is shippable today.
Phase 4: User Story 7 β Comprehensive automated test matrix exists and runs in CI (Priority: P1)β
Goal: make test-rbac is the single CI signal; adding a new gated route without a matrix entry fails the build. The matrix linter, realm-config drift check, and audit-log assertion helpers are wired together.
Why before US3-US6: Subsequent Python phases (US3, US4, US6) ship with matrix entries + parameterised tests; that requires US7's plumbing to already be live. US1's per-route migrations (Phase 3) used the bare-bones matrix driver; US7 hardens it for the rest of the migrations.
Independent Test: make test-rbac runs to completion locally in β€10min. make test-rbac-jest and make test-rbac-pytest are also runnable independently. A deliberately-introduced unprotected route in a throwaway commit makes the suite fail with a specific message naming the route.
Maps to: Story 7, FR-008, FR-009, FR-010, SC-006, SC-008.
- T054 [US7] Implement
tests/rbac/unit/py/matrix_driver.py: pytest-collected base class that loadstests/rbac/rbac-matrix.yaml, parameterises over each(route Γ persona)for entries withsurface β {supervisor, mcp, dynamic_agents, rag, slack_bot}, and provides per-surface helpers (call_supervisor,call_mcp,call_da,call_rag,call_slack_event) to invoke each surface with a persona token - T055 [P] [US7] Implement
tests/rbac/unit/py/conftest.py(separate fromtests/rbac/conftest.pyto avoid recursion): registers the matrix driver, providesaudit_collectionfixture pointing at e2e-compose Mongo, providesclean_authz_decisionsautouse fixture that drops the collection between test classes - T056 [US7] Implement
tests/rbac/e2e/playwright.config.ts: configures Playwright to read base URL fromE2E_UI_URL(defaulthttp://localhost:3000), usestests/rbac/fixtures/keycloak.tsto mint persona tokens, captures audit-log assertions via API calls. Test artifacts undertests/rbac/e2e/test-results/ - T057 [P] [US7] Implement Playwright spec
tests/rbac/e2e/story-1-admin-ui.spec.tscovering Story 1 acceptance scenarios end-to-end (real browser, real Keycloak login) - T058 [P] [US7] Implement
tests/rbac/e2e/story-7-matrix-completeness.spec.ts: asserts every matrix entry has a corresponding Jest or pytest result file; this surfaces "matrix entry exists but no test runs it" gaps - T059 [US7] Wire
make test-rbac-pytesttoPYTHONPATH=. uv run pytest tests/rbac/unit/py -v. Wiremake test-rbac-e2etocd ui && npx playwright test --config ../tests/rbac/e2e/playwright.config.ts - T060 [P] [US7] Add
.github/workflows/test-rbac.yamlperquickstart.mdΒ§"How CI runs this": ubuntu-latest runner, sets up node 20 + uv, runsCOMPOSE_PROFILES="rbac,caipe-ui,caipe-supervisor,caipe-mongodb,dynamic-agents,rag,all-agents,slack-bot" docker compose -f docker-compose.dev.yaml -f docker-compose/docker-compose.e2e.override.yaml pullandmake test-rbac, uploads logs on failure - T061 [US7] Add a deliberately-broken sample to
tests/rbac/unit/py/test_linter_smoke.pythat imports the linter, points it at a fixture route directory containing a route with no matrix entry, and asserts the linter exits non-zero with a message containing the route path (validates SC-006) - T062 [US7] Verify SC-008: time
make test-rbacend-to-end on M-series Mac, record the wall-clock time intests/rbac/PERFORMANCE.md. If >10 min: flag as a Phase 11 (Polish) task and add--workers=4to Playwright config
Checkpoint: Test infrastructure complete. Every later phase can ship with: (1) realm-config seeding, (2) matrix entry, (3) parameterised test that's automatically picked up β no per-phase test plumbing needed.
Phase 5: User Story 3 β Every agent MCP server is Keycloak-gated (Priority: P1)β
Goal: Every MCP server validates the bearer against Keycloak JWKS, then gates each tool call on require_rbac_permission(token, '<agent>_mcp', 'read'|'write'). Shared-key auth is removed (FR-012).
Independent Test: For each MCP server, parameterised pytest POSTs tools/list and a representative tools/call with each persona's token; asserts the matrix.
Maps to: Story 3, FR-002, FR-003, FR-007 (Py side), FR-012.
Realm config + matrix entriesβ
- T063 [US3] Add to
deploy/keycloak/realm-config.jsonper FR-006: 12 resourcesargocd_mcp,aws_mcp,jira_mcp,github_mcp,pagerduty_mcp,splunk_mcp,confluence_mcp,webex_mcp,slack_mcp,komodor_mcp,aigateway_mcp,backstage_mcpβ each with scopesread,write. Bindchat_usertoread,team_membertoread+write(within team),adminto all - T064 [US3] Add matrix entries to
tests/rbac/rbac-matrix.yamlper MCP Γ representative tool: at minimumtools/list(scoperead) and one mutatingtools/call(scopewrite). For agents with no mutating tools (e.g.splunkis read-only), include only thereadentry and anotes:line explaining the omission
Python helper wiring (Starlette middleware for MCP servers)β
- T065 [US3] Implement
ai_platform_engineering/agents/common/mcp-auth/keycloak_middleware.py: Starlette ASGI middleware that callsvalidate_bearer_jwt(T020) on every request, raises 401 on failure, setscurrent_bearer_tokenContextVar (existing inmcp-auth/token_context.py) for tool dispatch - T066 [P] [US3] Modify
ai_platform_engineering/agents/common/mcp-auth/middleware.py: addoauth2_keycloakmode that wires the new middleware (alongside existingnone,shared_key,oauth2); set as the default whenMCP_AUTH_MODE=oauth2_keycloak. Keepshared_keymode in code but emit a startuploguruERROR if it's selected - T067 [US3] Add a
tools/call-time hook in each MCPserver.py: a wrapper_authz_wrap(tool_name, scope, fn)that callsawait require_rbac_permission(current_bearer_token.get(), '<agent>_mcp', scope)before delegating to the tool. Apply per-agent in T068βT078 below
Per-agent MCP migrations (one task per MCP server β parallelisable)β
- T068 [P] [US3] Migrate
ai_platform_engineering/agents/argocd/mcp/mcp_argocd/server.py: switchMCP_AUTH_MODEdefault tooauth2_keycloak, wrap every tool function with_authz_wrap('argocd_mcp', '<read|write>', ...), removeSharedKeyMiddlewareregistration. Add torealm-config.jsonaudience foraud=caipe-platform - T069 [P] [US3] Migrate
ai_platform_engineering/agents/jira/mcp/mcp_jira/server.py: same pattern (jira_mcp,read/write) - T070 [P] [US3] Migrate
ai_platform_engineering/agents/github/mcp/mcp_github/__main__.py(noserver.pyβ entry is__main__.py): same pattern (github_mcp,read/write) - T071 [P] [US3] Migrate
ai_platform_engineering/agents/pagerduty/mcp/mcp_pagerduty/server.py(pagerduty_mcp) - T072 [P] [US3] Migrate
ai_platform_engineering/agents/splunk/mcp/mcp_splunk/server.py(splunk_mcpβ read-only) - T073 [P] [US3] Migrate
ai_platform_engineering/agents/confluence/mcp/mcp_confluence/server.py(confluence_mcp) - T074 [P] [US3] Migrate
ai_platform_engineering/agents/webex/mcp/mcp_webex/__main__.py(webex_mcp) - T075 [P] [US3] Migrate
ai_platform_engineering/agents/komodor/mcp/mcp_komodor/server.py(komodor_mcp) - T076 [P] [US3] Migrate
ai_platform_engineering/agents/backstage/mcp/mcp_backstage/server.py(backstage_mcp) - T077 [P] [US3] Migrate
ai_platform_engineering/agents/victorops/mcp/mcp_victorops/server.py(victorops_mcpβ note: not in plan's 12-agent list but exists in repo; treat the same) - T078 [P] [US3] Migrate
ai_platform_engineering/agents/netutils/mcp/mcp_netutils/server.py(netutils_mcpβ same as T077)
AWS, Slack, AIGateway: these three MCPs from the plan's list have no
server.pyor__main__.pyin the current codebase (verified Phase 0). If they exist as in-repo agents by the time this phase runs, mirror the T068 pattern. If not (i.e., still planned), add a placeholder matrix entry withnotes: "agent MCP server not yet implemented"and askip_reasonin expectations. Add a test that fails when the agent appears in the codebase to force re-visiting.
Tests for US3β
- T079 [P] [US3] Implement
tests/rbac/unit/py/test_mcp_auth_jwt.py: parameterised over the 10 in-repo MCPs Γ 6 personas; uses the e2e-compose stack to spin up each MCP via its existing__main__.py; asserts 401 on missing/expired/wrong-issuer token, 200/403 per matrix - T080 [US3] Implement
tests/rbac/unit/py/test_mcp_shared_key_removed.py: greps forSharedKeyMiddlewareregistrations inai_platform_engineering/agents/; asserts ZERO matches (FR-012). Runs as part ofmake test-rbac-pytest
Checkpoint: Every MCP server in the repo gates on Keycloak. Shared-key auth removed.
Phase 6: User Story 2 β Supervisor enforces Keycloak before delegating to agents (Priority: P1)β
Goal: Supervisor's existing JwtUserContextMiddleware + OBO mint + httpx_client_factory chain is proven by tests to produce a downstream MCP Authorization header whose JWT sub resolves to the original user. Implementation is already there post-merge; this phase locks it down with tests and adds the missing PDP gate at the supervisor's A2A entry.
Independent Test: Stand up supervisor + a stub MCP server (tests/rbac/fixtures/stub_mcp.py) that records inbound headers. Send A2A requests with each persona token. Assert: inbound JWT validated; OBO minted; stub MCP sees Authorization: Bearer <obo> whose decoded sub is the persona's keycloak_sub and act.sub is the supervisor service account.
Maps to: Story 2, FR-002 (verify), FR-003 (apply at supervisor), FR-007 (audit at supervisor).
Realm config + matrix entriesβ
- T081 [US2] Add to
deploy/keycloak/realm-config.json: clientcaipe-supervisorwithserviceAccountenabled; impersonation policy granting iturn:ietf:params:oauth:grant-type:token-exchangefor users inchat_userrealm role (percall-sequences.mdFlow 3). Verify by inspectingrealm-config.jsonβclientsarray - T082 [US2] Add matrix entries for the supervisor surface (
surface: supervisor):POST /tasks/send(rpc-equivalent) with one entry per agent invocation βargocd_agent.list_apps(resourceargocd_mcp, scoperead), one mutating tool (argocd_agent.delete_appβwrite), etc.
Supervisor PDP gate at A2A entry (defense-in-depth β MCP already gates)β
- T083 [US2] Modify
ai_platform_engineering/multi_agents/platform_engineer/protocol_bindings/a2a/agent_executor.pyexecute(): before invoking the graph, callawait require_rbac_permission(ctx.token, 'supervisor', 'invoke')(NEW resource β add to T081's realm config too); raise A2A error on deny withreason=DENY_NO_CAPABILITY - T084 [US2] Add resource
supervisor(scopesinvoke,manage) todeploy/keycloak/realm-config.json(chained off T081); bindchat_usertoinvoke,admintomanage
Tests for US2β
- T085 [US2] Implement
tests/rbac/fixtures/stub_mcp.py: minimal Starlette MCP that records every request'sAuthorizationheader and tool name in an in-memory list, exposes/_test/captured_requestsfor assertions. Used by T086 + T108 (US6) - T086 [US2] Implement
tests/rbac/unit/py/test_supervisor_obo.py: parameterised over 4 cases per Story 2 acceptance scenario β (a) valid bob token β 200 + stub_mcp sees correct OBO; (b) expired token β 401, no graph stream; (c) wrong-issuer token β 401, JWKS not re-fetched within 60s (asserted via JWKS cache mock); (d) chain of 2 agents β every hop seessub=bob.keycloak_sub
Checkpoint: Supervisor proven correct under test. A2A entry now also has a PDP gate (defense-in-depth).
Phase 7: User Story 4 β RAG hybrid Keycloak + Mongo KB ACL (Priority: P1)β
Goal: RAG /v1/ingest and /v1/query gate on Keycloak (rag#ingest, rag#retrieve); per-KB visibility computed as union of TeamKbOwnership (Mongo) and per-KB realm roles (kb_reader:<id>, kb_ingestor:<id>).
Independent Test: Seed two KBs (team-a-docs, team-b-docs) with sentinel docs. Call /v1/query as each persona. Assert: alice sees both, carol sees only team-a-docs, bob sees nothing in strict mode, dave gets 403.
Maps to: Story 4, FR-002, FR-003, FR-013.
Realm config + matrixβ
- T087 [US4] Add resource
rag(scopesingest,retrieve,manage) todeploy/keycloak/realm-config.json; bindchat_usertoretrieve,kb_ingestorrealm role toingest+retrieve. Note: per-KB realm roles (kb_reader:<id>,kb_ingestor:<id>) are created by team-management UI on KB provision (existing 098 implementation); this phase only consumes them - T088 [P] [US4] Add matrix entries for RAG:
(POST /v1/ingest, rag, ingest)and(POST /v1/query, rag, retrieve). Add per-persona expectations including the per-KB filter scenario asnotes
RAG server modificationsβ
- T089 [US4] Modify
ai_platform_engineering/knowledge_bases/rag/server/src/server/restapi.py: registerJwtUserContextMiddlewareat the FastAPI app level; addDepends(require_rbac_permission_dep('rag', 'ingest'))to/v1/ingestroute,Depends(require_rbac_permission_dep('rag', 'retrieve'))to/v1/query - T090 [US4] Modify
ai_platform_engineering/knowledge_bases/rag/server/src/server/rbac.py: implementaccessible_kbs(user_token, db) -> set[str]perdata-model.mdΒ§E6 β union of (Mongoteam_kb_ownershiprows where user is inownerTeamId) βͺ (KB ids where user has realm rolekb_reader:<id>orkb_ingestor:<id>) - T091 [US4] Modify
ai_platform_engineering/knowledge_bases/rag/server/src/server/query_service.py: post-filter result chunks bychunk.kb_id β accessible_kbs(user); if the resulting set is empty, return 200 + empty results (NOT 403 β that distinction matters for non-malicious users in strict deployments) - T092 [US4] Modify
ai_platform_engineering/knowledge_bases/rag/server/src/server/ingestion.py: assertrequest.kb_id β accessible_kbs(user)AND user haskb_ingestor:<kb_id>realm role (or is admin) before writing; raiseHTTPException(403, reason=DENY_NO_CAPABILITY)otherwise
Tests for US4β
- T093 [P] [US4] Implement
tests/rbac/fixtures/rag_seed.py: helperseed_kbs(['team-a-docs', 'team-b-docs'])that creates the KBs in Mongoteam_kb_ownership, creates the per-KB realm roles in Keycloak (viakcadmshell-out), assignscarol_kb_ingestortokb_ingestor:team-a-docs. Runs once per test session - T094 [US4] Implement
tests/rbac/unit/py/test_rag_query_per_kb.py: parameterised over the 4 personas relevant to Story 4 β alice (sees both), carol (sees only team-a-docs), bob (sees neither in strict deployment), dave (403). Uses T093's seed - T095 [P] [US4] Implement
tests/rbac/unit/py/test_rag_ingest_per_kb.py: covers Story 4 acceptance scenarios 1+2 β carol can ingest into team-a-docs but not team-b-docs
Checkpoint: RAG runs the hybrid gate end-to-end. Story 4 demonstrably shippable.
Phase 8: User Story 6 β Custom (Dynamic) Agents are bound to Keycloak (Priority: P1) biggest deltaβ
Goal: All five layers from call-sequences.md Flow 4b β BFF chat-stream gate, da-proxy.ts no-X-User-Context, DA backend JWT middleware, DA backend PDP defense-in-depth, MCP-call-from-DA carries fresh per-request OBO.
Independent Test: Three agents seeded β private-eve, team-a-shared, global-public. Each persona attempts view, invoke, manage on each. Assert via Playwright (BFF) and pytest (DA backend probed directly with a forged X-User-Context β must be ignored).
Maps to: Story 6, FR-002, FR-003, FR-004, FR-005.
Realm configβ
- T096 [US6] Extend the resource
dynamic_agentindeploy/keycloak/realm-config.json(created in T034) to support per-agent instance resources: conventiondynamic_agent:<agent_id>with same three scopes. Implement seeding via the existing dynamic-agent provisioning code path so creating a DA in the UI also creates the Keycloak resource (out-of-scope code change is minimal β wire intoui/src/app/api/dynamic-agents/route.ts's POST handler) - T097 [P] [US6] Add matrix entries for:
POST /api/v1/chat/stream/startper representative agent (private-eve,team-a-shared,global-public);dynamic_agentsadmin routes (already in T047 from US1)
BFF gate (TS)β
- T098 [US6] Modify
ui/src/app/api/v1/chat/stream/start/route.ts: parseagent_idfrom request body before any DA call; callawait requireRbacPermission(session, 'dynamic_agent:' + agent_id, 'invoke'); on deny, return 403 without opening the SSE stream - T099 [P] [US6] Modify
ui/src/lib/da-proxy.ts: removeuserContextbase64 construction fromauthenticateRequest();proxySSEStream()no longer addsX-User-Contextheader; pass throughAuthorization: Bearer <session.accessToken>header instead. Update the function signature to accept{ bearer: string }instead of{ userContext: string } - T100 [US6] Update jest test
ui/src/app/api/__tests__/da-proxy.test.ts(create if missing): asserts the outbound request to DA hasAuthorizationheader but NOTX-User-Context
DA backend (Python)β
- T101 [US6] Implement
ai_platform_engineering/dynamic_agents/src/dynamic_agents/auth/token_context.py:current_user_token: ContextVar[str | None] = ContextVar('current_user_token', default=None)β mirror of supervisor'sai_platform_engineering/utils/auth/token_context.py - T102 [P] [US6] Implement
ai_platform_engineering/dynamic_agents/src/dynamic_agents/auth/jwt_middleware.py: Starlette ASGI middleware mirroringai_platform_engineering/utils/auth/jwt_user_context_middleware.py; on every request, validates JWT (T020), setscurrent_user_tokenContextVar, sets a DA-localcurrent_user_contextContextVar with{sub, email, roles}. On invalid token: respond 401 immediately - T103 [US6] Modify
ai_platform_engineering/dynamic_agents/src/dynamic_agents/auth/auth.pyget_user_context(): replace base64X-User-Contextdecode withget_jwt_user_context()from the new ContextVar (FR-004). Delete the_decode_user_context_headerhelper and any remainingrequest.headers.get('X-User-Context')reads - T104 [P] [US6] Implement
ai_platform_engineering/dynamic_agents/src/dynamic_agents/auth/keycloak_authz.py: thin wrapper exportingrequire_rbac_permission(re-exports fromai_platform_engineering/utils/auth/keycloak_authz.py) plus arequire_da_permission(agent_id, scope)FastAPI dependency that constructsdynamic_agent:<agent_id>and calls the underlying helper - T105 [P] [US6] Implement
ai_platform_engineering/dynamic_agents/src/dynamic_agents/auth/obo_exchange.py: copy ofai_platform_engineering/utils/obo_exchange.pyadjusted to useKEYCLOAK_DA_CLIENT_IDenv var; cached (TTL β₯ 30s before expiry) - T106 [US6] Modify
ai_platform_engineering/dynamic_agents/src/dynamic_agents/auth/access.pycan_view_agent,can_use_agent,can_access_conversation: after the existing local CEL eval, also callrequire_rbac_permission(token, 'dynamic_agent:<agent_id>', '<view|invoke|manage>'); if EITHER local CEL OR Keycloak denies β deny. (Defense-in-depth pattern) - T107 [US6] Modify
ai_platform_engineering/dynamic_agents/src/dynamic_agents/services/agent_runtime.py: at the start of everyinvoke()/chat_stream()entry point, setcurrent_user_tokenContextVar from the request's bearer; remove the instance attributeself._auth_bearerentirely (callers must use ContextVar). Verify no remaining_auth_bearerreferences withrg -n '_auth_bearer' ai_platform_engineering/dynamic_agents/ - T108 [US6] Modify
ai_platform_engineering/dynamic_agents/src/dynamic_agents/services/mcp_client.py: implement_build_httpx_client_factory()mirroringai_platform_engineering/utils/a2a_common/base_langgraph_agent.py:255(current_user_token.get()βAuthorizationheader per request); replace the staticauth_bearer=β¦argument inMultiServerMCPClientconstruction (FR-005)
Tests for US6β
- T109 [US6] Implement Playwright spec
tests/rbac/e2e/story-6-dynamic-agents.spec.ts: covers all 5 acceptance scenarios for Story 6 β including alice/bob/eve hittingprivate-eve,team-a-shared,global-publicand asserting the matrix - T110 [US6] Implement
tests/rbac/unit/py/test_da_jwt_middleware.py: directly POSTs to DA with (a) no auth β 401, (b) valid bob β 200, (c) forgedX-User-Context: <base64({"is_admin":true})>AND no Authorization header β 401 β proves header is no longer trusted (Story 6 acceptance scenario 3, the security-critical test) - T111 [P] [US6] Implement
tests/rbac/unit/py/test_da_mcp_obo.py: stand up T085's stub_mcp; configure a DA to use it; chat with the DA as bob; assert stub_mcp's capturedAuthorizationheader decodes to a JWT withsub=bob.keycloak_subAND a freshiat(β€5s old β proves per-request mint, not cached) - T112 [US6] Implement
tests/rbac/unit/py/test_da_no_xusercontext.py: greps the entireai_platform_engineering/dynamic_agents/tree forX-User-Context; asserts only matches are in test fixtures or audit-log lines (SC-002)
Checkpoint: DA fully Keycloak-bound. The largest security-relevant delta is locked down by tests.
Phase 9: User Story 5 β Slack commands run with the user's identity, not the bot's (Priority: P2)β
Goal: Slack bot uses Keycloak impersonate_user(slack_userβkeycloak_sub) token-exchange per command; supervisor sees sub=user, act.sub=bot JWT.
Independent Test: Send a slash command from a linked user via the Slack Events test harness; capture supervisor's incoming Authorization header; decode JWT; assert sub == bob.keycloak_sub and act.sub == caipe-slack-bot's service-account sub.
Maps to: Story 5, FR-011.
Realm configβ
- T113 [US5] Add to
deploy/keycloak/realm-config.json: clientcaipe-slack-bot(already exists in 098 β verify); add token-exchange + impersonation policies granting it the right to mint OBO tokens for users inchat_userrealm role. Add resourceslack(scopesuse,register); bindchat_usertouse, onlycaipe-slack-botservice account toregister - T114 [P] [US5] Add matrix entries:
surface: slack_botfor representative slash commands β/caipe list argocd apps(resourceargocd_mcp, scoperead),/caipe link(resourceslack, scopeuse)
Slack bot wiringβ
- T115 [US5] Modify
ai_platform_engineering/integrations/slack_bot/app.py: every command handler (e.g.handle_app_mention,handle_slash_command) must callawait impersonate_user(keycloak_sub)on the resolvedkeycloak_subfrom the Slack-link metadata, then build the supervisor request withAuthorization: Bearer <obo>. Referencecall-sequences.mdFlow 5 - T116 [P] [US5] Modify
ai_platform_engineering/integrations/slack_bot/utils/rbac_middleware.py: remove the channel-allowlist gate (it becomes a Keycloakslack#usescope check); on unlinked user, respond with the linking instructions (FR-025 from 098) - T117 [US5] Verify
ai_platform_engineering/integrations/slack_bot/utils/obo_exchange.py:89(impersonate_user) still works against the now-token-exchange-enabled Keycloak (T005) by manual smoke against the dev compose
Tests for US5β
- T118 [US5] Implement
tests/rbac/unit/py/test_slack_obo.py: uses Slack Bolt's test harness (SocketModeRequestmock) to fire a slash command frombob_chat_user; intercepts the supervisor HTTP call; decodes JWT; assertssub == bob.keycloak_subANDact.sub == caipe-slack-bot's sub - T119 [P] [US5] Implement
tests/rbac/unit/py/test_slack_unlinked_user.py: covers Story 5 acceptance scenarios 2 + 4 β unlinked user gets linking instructions, and a user lackingteam_memberfor a channel-mapped team gets the FR-031 deny - T120 [US5] Implement Playwright spec
tests/rbac/e2e/story-5-slack.spec.ts: end-to-end Slack flow via the bot's HTTP webhook endpoint (no real Slack β uses the test harness)
Checkpoint: Slack commands carry user identity end-to-end. Per-user audit attribution finally works.
Phase 10: User Story 8 β docs/docs/security/rbac/ is the canonical reference (Priority: P2)β
Note (post-split): the canonical RBAC reference was previously a single file at
docs/docs/specs/098-enterprise-rbac-slack-ui/how-rbac-works.md. It has been split into focused files underdocs/docs/security/rbac/(index.md,architecture.md,workflows.md,usage.md,file-map.md). Tasks below have been retargeted accordingly. The old path still exists as a redirect stub.
Goal: docs/docs/security/rbac/ accurately reflects the post-migration state. File map is auto-validated.
Independent Test: A junior reviewer answers a 10-question quiz auto-generated from the file map and component sections; passes 9/10 in <5 min.
Maps to: Story 8, FR-014, SC-007.
- T121 [US8] Implement
scripts/validate-rbac-doc.py: parses the table indocs/docs/security/rbac/file-map.md; asserts every listed file exists; asserts every authz-relevant production file (referenced byrequireRbacPermission/require_rbac_permissioncalls or byJwtUserContextMiddlewareregistrations) appears in the table; exits non-zero on drift (FR-014). Wire intomake test-rbacafter the matrix linter - T122 [US8] Update
docs/docs/security/rbac/architecture.mdcomponent sections: add a NEW Component for "Python RBAC helpers" (T020βT024) with env vars table, error responses, file paths; update Component 5 (Dynamic Agents) to reflect the post-Phase-8 state; add a "Migrated from 098 partial implementation" callout box on every section affected - T123 [P] [US8] Update
docs/docs/security/rbac/file-map.mdtable: add the new files from Phases 2, 6, 7, 8, 9 βtests/rbac/**,ai_platform_engineering/utils/auth/{jwks_validate,keycloak_authz,audit,realm_extras}.py,ai_platform_engineering/dynamic_agents/.../auth/{jwt_middleware,token_context,keycloak_authz,obo_exchange}.py,deploy/keycloak/realm-config-extras.json,scripts/validate-rbac-{matrix,doc,realm-config}.py,scripts/extract-rbac-resources.py - T124 [US8] Update sequence diagrams in
docs/docs/security/rbac/workflows.md: the existing AgentGateway end-to-end diagram is fine β add a sistersequenceDiagramfor the non-AG paths (BFF β Supervisor β MCP and BFF β DA β MCP), and add a fresh diagram for the per-agent gate at the chat endpoint introduced by Phase 8 - T125 [P] [US8] Generate the 10-question quiz at
docs/docs/specs/102-comprehensive-rbac-tests-and-completion/quiz.md. Sample questions: "Which env var controls the PDP cache TTL?" (RBAC_CACHE_TTL_SECONDS), "Which file maps Keycloak resources to PDP-unavailable fallback rules?" (deploy/keycloak/realm-config-extras.json), etc. Include answer key - T126 [US8] Run
python scripts/validate-rbac-doc.pyafter all updates. Confirm exit 0. Run the quiz on a junior reviewer (or the team's "least-RBAC-aware" engineer); record score inquiz.md. Pass = 9/10
Checkpoint: Documentation is accurate, validated, and reviewer-approved. Spec 098 has its companion document brought up to truth.
Phase 11: Polish & Cross-Cutting Concernsβ
Purpose: Cleanup, perf budget verification, and follow-up tickets. Runs after all 8 user stories are green.
- T127 [P] Delete
requireAdminViewfromui/src/lib/api-middleware.ts(callers all migrated by T040βT049). Runnpx tsc --noEmitto confirm no remaining type references - T128 [P] Delete
SharedKeyMiddlewareandMCP_AUTH_MODE=shared_keycode path fromai_platform_engineering/agents/common/mcp-auth/middleware.py(FR-012; was kept with deprecation warning in T066). Updatemcp-auth/README.mdto remove theshared_keydocumentation - T129 [P] Verify SC-001:
rg -n 'requireAdmin\(session\)|canViewAdmin' ui/src/app/api/{admin,dynamic-agents,mcp-servers,teams,agents}/returns ZERO matches. Verify SC-002:rg -n "X-User-Context" ai_platform_engineeringreturns only test fixtures and audit-log emit lines - T130 [P] Verify SC-003: every matrix entry under
ui_bffsurface has at least 1 allow + 1 deny test result file. Runpython scripts/validate-rbac-coverage.py(NEW β small helper that counts test functions per matrix id) - T131 [P] Verify SC-004:
pytest tests/rbac/unit/py --collect-only -q | wc -lβ₯ (#python services Γ 4 minimum cases) - T132 [P] Verify SC-005 + SC-008: time
make test-rbacend-to-end; record intests/rbac/PERFORMANCE.md. Both β€10min local, β€12min CI. If exceeded: enable Playwrightworkers: 4and re-time - T133 Update
agents.mdandCLAUDE.md"Active Technologies" sections to reflect Phase 5β10 changes (mentiontests/rbac/, the new Python helpers, the matrix linter) - T134 [P] Open follow-up issues for the items in
research.md"Open follow-ups (NOT for this PR)": (1)authz_decisionsretention (expireAfterSeconds), (2) Cross-process PDP cache (Redis), (3) Keycloak realm export drift detection, (4) Per-tool MCP scopes. File viagh issue create; do NOT block this PR on them - T135 [P] Run final
make lint+make test+make caipe-ui-tests+make test-rbac. Confirm all four green. Updateprebuild/feat/comprehensive-rbacPR#1257description with completion summary linking to thistasks.md
Final Checkpoint: All 8 user stories ship together in PR #1257 (FR-015). Doc is current. Tests are green. Follow-ups are tracked, not lost.
Dependencies & Execution Orderβ
Phase Dependenciesβ
- Phase 1 (Setup): No deps β start immediately.
- Phase 2 (Foundational): Depends on Phase 1. BLOCKS every user-story phase.
- Phase 3 (US1): Depends on Phase 2. Independent of Phases 4β10.
- Phase 4 (US7): Depends on Phase 2. Should run before Phases 5β9 because subsequent phases reuse the matrix driver and Playwright config it builds.
- Phase 5 (US3): Depends on Phase 2 + Phase 4. Independent of Phases 6β10.
- Phase 6 (US2): Depends on Phase 2 + Phase 4 + Phase 5 (uses MCP-side tests as integration target via stub_mcp).
- Phase 7 (US4): Depends on Phase 2 + Phase 4. Independent of others.
- Phase 8 (US6): Depends on Phase 2 + Phase 4 + Phase 5 (DA's MCP calls go through MCP-side gate). Largest delta β schedule carefully.
- Phase 9 (US5): Depends on Phase 2 + Phase 4 + Phase 6 (Slack OBO calls supervisor with the same OBO pattern). Independent of Phases 7β8.
- Phase 10 (US8): Depends on all prior story phases (doc reflects post-migration state). MUST be last user-story phase.
- Phase 11 (Polish): Depends on all prior phases.
Within Each User Storyβ
- TDD-first for foundational helpers (T025βT028 RED before T020βT023 implementations).
- Matrix entry first, then code (matrix entry causes the linter to fail until code lands β TDD-RED-style for the linter itself).
- Realm config first, then code that calls the new resource (otherwise PDP returns
DENY_RESOURCE_UNKNOWN). - Tests in same commit as the code change they cover (every implementation task has a matching test task β they MUST land together).
Parallel Opportunities (within a single phase)β
- Phase 2: T009/T010/T011 + T015/T016 + T020/T021/T022/T023 + T025/T026/T027/T028 β most foundational tasks are file-disjoint and parallelisable; ~12 of 25 Phase-2 tasks marked
[P]. - Phase 3 (US1): T040βT049 are 10 file-disjoint route-cluster migrations β can run in parallel by 10 developers (or one developer in 10 separate commits for clean review).
- Phase 5 (US3): T068βT078 are 11 file-disjoint per-MCP migrations β same parallelism story.
- Phase 8 (US6): T101/T102/T104/T105 (NEW DA auth files) all parallel; T106/T107/T108 (modifications) sequential within
dynamic_agents/services/. - Cross-phase: Phases 5, 7, 8 can run in parallel after Phase 4 completes if multiple devs are on the project (distinct surface areas).
Parallel Example: Phase 3 (US1)β
# Once Phase 2 (Foundational) is checkpointed, kick these off concurrently:
# Realm config (single editor; do this first, sequentially)
Task: T033 β Add admin_ui, team, mcp_server resources to realm-config.json
Task: T034 β Add dynamic_agent resource
# Matrix entries (single editor)
Task: T035 β Add 30 admin route entries to rbac-matrix.yaml
Task: T036 β Add 10 entries for dynamic-agents/mcp-servers/teams/agents
# Then 10 parallel route migrations across 10 PR commits / 10 devs
Task: T040 β Migrate ui/src/app/api/admin/users/**/route.ts
Task: T041 β Migrate ui/src/app/api/admin/teams/**/route.ts
Task: T042 β Migrate ui/src/app/api/admin/roles/**/route.ts
Task: T043 β Migrate ui/src/app/api/admin/slack/**/route.ts
Task: T044 β Migrate ui/src/app/api/admin/audit-{events,logs}/route.ts
Task: T045 β Migrate ui/src/app/api/admin/{nps,feedback,metrics,β¦}/route.ts
Task: T046 β Migrate ui/src/app/api/admin/stats/**/route.ts
Task: T047 β Migrate ui/src/app/api/dynamic-agents/**/route.ts
Task: T048 β Migrate ui/src/app/api/mcp-servers/**/route.ts
Task: T049 β Migrate ui/src/app/api/agents/tools/route.ts
The final assembly task (T053) waits for all 10 to complete and runs the suite end-to-end.
Implementation Strategyβ
MVP First (User Story 1 only)β
- Complete Phase 1 (Setup).
- Complete Phase 2 (Foundational) β CRITICAL gate.
- Complete Phase 3 (US1).
- STOP and VALIDATE:
make test-rbac-jestgreen; admin UI is fully Keycloak-only; demo internally. - This alone is a real security improvement and a defensible incremental ship.
Incremental Delivery (recommended order)β
- Setup + Foundational β foundation ready (Phases 1+2).
- Add Story 1 β ship admin-UI-only (Phase 3).
- Add Story 7 β CI signal locked (Phase 4).
- Add Stories 3 + 2 + 4 β Python services hardened (Phases 5+6+7).
- Add Story 6 β DA fully migrated (Phase 8). Largest single delta β most thorough review.
- Add Stories 5 + 8 β Slack + docs (Phases 9+10).
- Polish (Phase 11).
Parallel Team Strategy (2 developers, ~6 working days)β
| Day | Dev A | Dev B |
|---|---|---|
| 1 | Phase 1 + start Phase 2 helpers | Phase 1 + start Phase 2 fixtures |
| 2 | Finish Phase 2 helpers (T020βT024) | Finish Phase 2 fixtures + lints (T015βT019, T009βT011) |
| 3 | Phase 3 (US1) routes T040βT049 | Phase 4 (US7) test infrastructure T054βT062 |
| 4 | Phase 5 (US3) MCP migrations T068βT078 | Phase 6 (US2) supervisor + Phase 7 (US4) RAG |
| 5 | Phase 8 (US6) DA migration β pair on this | Phase 8 (US6) DA migration β pair on this |
| 6 | Phase 9 (US5) Slack | Phase 10 (US8) doc + Phase 11 polish |
Single-developer strategy (~10 working days)β
Sequential phases in priority order; expect 1β1.5 days per phase except Phase 8 (DA) which is 2 days.
Notesβ
[P]tasks operate on disjoint files and have no in-phase dependencies β safe to parallelise.[Story]label maps a task to the user story it serves; setup/foundational/polish tasks have no story label.- Every test task ASSUMES TDD: write the test, see it fail, then implement, then see it pass. Phase 2 explicitly calls this out; later phases inherit the discipline.
- Tasks without a file path are forbidden by the format. If an action doesn't have a single file path (e.g., "verify SC-001"), the file path is the verification artifact (e.g., the
rgcommand output captured in PR description). - Single-PR mandate (FR-015): every commit lands on
prebuild/feat/comprehensive-rbacand rolls into PR#1257. Do not branch off this branch for individual phases. - Stop at any checkpoint to validate the story independently. Each story IS independently testable per
spec.md. - Avoid: cross-story dependencies that break independence (e.g., a US5 task that requires US8's doc to exist), same-file conflicts in
[P]tasks, vague tasks without file paths.
Appendix: Deferred/Operator Follow-Upsβ
This appendix replaces the former root-level BLOCKERS.md scratch tracker. Keep
new deferred RBAC work in this spec so the task list remains the source of
truth; completed historical notes stay in git history instead of being copied
forward.
Operator verificationβ
- Verify the Dynamic Agents to AgentGateway MCP chain after the bearer
forwarding fix. Restart
dynamic-agents, send a web chat to an agent that uses Jira/Confluence/Argo MCP tools, and confirmdynamic-agentsno longer logsHTTP 401 error connecting to http://agentgateway:4000/mcp/.... Confirm AgentGateway logs show MCP-bound requests carryingAuthorization: Bearer .... For a negative check, setDA_REQUIRE_BEARER=true, restart Dynamic Agents, and verify requests without BFF bearer propagation fail withcode: missing_bearer; then restore the previous environment. - Verify the supervisor PDP gate in a live stack. Set
SUPERVISOR_PDP_GATE_ENABLED=true, restart the supervisor, and confirm achat_usercan chat while a user withoutchat_userreceives the standardized RBAC denial:{"code":"rbac_denied","reason":"missing_role","action":"contact_admin"}. Stop Keycloak and confirm a non-bootstrap-admin receivescode: pdp_unavailable, then restart Keycloak and unset the gate flag. - Run Slack OBO live verification against a running stack with
scripts/verify-slack-obo.sh. Confirm the decoded access token has the expectedsub,azp, andact.subclaims, and use the script's inline failure hints for missing token-exchange policy, missing impersonation permission, or disabled Keycloak token-exchange support. This verifies the implementation behind T117/T118; Slack JIT-specific live checks remain in spec 103 Phase 7.KEYCLOAK_URL=http://localhost:7080 \
KEYCLOAK_REALM=caipe \
KEYCLOAK_BOT_CLIENT_ID=caipe-slack-bot \
KEYCLOAK_BOT_CLIENT_SECRET=... \
TARGET_USER=admin \
./scripts/verify-slack-obo.sh - Wire the RBAC Playwright harness into GitHub Actions once the live stack
can be provisioned in CI. The harness already exists under
ui/e2e/rbac/withnpm run test:e2e:rbac; the remaining work is infrastructure provisioning for Keycloak, supervisor, Dynamic Agents, and the BFF, either via kind + Helm or a hosted preview stack. - Run the RAG document ACL migration before enabling
RBAC_DOC_ACL_TAGS_ENABLED=truein any environment with existing data: first dry-runpython3 scripts/rag-doc-acl-migration.py --milvus-uri http://localhost:19530 --dry-run, inspect the JSON summary, then run the command without--dry-run. Verify representative rows now containmetadata.acl_tags=["__public__"], restartrag-serverwith the feature flag enabled, and smoke-test a RAG chat query. Optionally tag a small subset with a real tag such asteam:platform-engand verify users outside that team no longer see those documents. - Tune the PDP cache TTL after
rbac_pdp_*metrics have enough production signal. This depends on the decision-cache observability added in Phase 11. - After live verification, rerun
make lintandmake testfrom the repo root before opening or updating the PR.
Product follow-upsβ
- Add UI support for assigning RAG
metadata.acl_tagsat ingest time. The current safe default is["__public__"]. - Add connector-side ACL tag automation so ingestors populate
document_metadata.metadata["acl_tags"]from source permissions such as Confluence space permissions, Jira project permissions, and Slack channel membership. - Remove
X-User-Contextforwarding entirely after one release cycle of soak onDA_REQUIRE_BEARER=true. The header is no longer authoritative, but is still forwarded temporarily for Dynamic Agents claim-hint compatibility.
Known pre-existing failuresβ
- Track and fix the pre-existing
ai_platform_engineering/dynamic_agents/tests/test_sse_error_sanitization.pyfailures caused by_generate_resume_sse_eventssignature drift. - Track and fix the pre-existing
ai_platform_engineering/multi_agents/tests/...test_ai.pyfailures confirmed during the RBAC branch validation cycle.