2026 03 16 Sessions Security Unification

Documentation for Plans

Sessions Security & Unification Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Unify authentication across all 4 backends with deny-by-default APP_GUARD, dedicated internal routes for service-to-service communication, tenant isolation (tenantId=spaceId), whistle hardening, and WebSocket security.

Architecture: UnifiedAuthGuard + RolesGuard registered as dual APP_GUARD (deny-by-default). Every route is protected unless explicitly @Public(). Service-to-service calls use /internal/* routes with @ServiceOnly(). Tenant isolation enforced via @TenantScoped() decorator. SSE endpoints remain @Public() with ticket-based auth planned for a future session.

Rollout strategy: Additive-then-subtractive — add @Public() to ALL currently-unguarded controllers FIRST, then register APP_GUARD (zero breakage), then remove @Public() from controllers that should be protected.

Important note: backend-api's AuthCoreModule.forRoot() is imported inside BackendAuthModule at APIs/backend-api/src/app/backend-auth/backend-auth.module.ts, NOT in app.module.ts. Since AuthCoreModule is @Global(), the APP_GUARD registration propagates to all routes.

Deferred to Post-S227

The following spec items are acknowledged but deferred:

Tech Stack: NestJS 11, Mongoose 8, @nestjs/jwt, Jest, @libs-api/auth-core

Spec: docs/superpowers/specs/2026-03-16-sessions-security-unification-design.md


Chunk 1: Foundation — UnifiedAuthGuard + Decorators

Task 1: UnifiedCaller Interface

Files:

Create libs/libs-api/auth-core/src/lib/interfaces/unified-caller.interface.ts with:

Create libs/libs-api/auth-core/src/lib/interfaces/__tests__/unified-caller.spec.ts:

Run: yarn nx test auth-core --testPathPattern=unified-caller Expected: PASS — all 6 tests

Add exports to libs/libs-api/auth-core/src/index.ts: UnifiedCaller, CallerTypeValue, AuthMethodValue, createUserCaller, createServiceCaller, createApiKeyCaller, createDevCaller

git add libs/libs-api/auth-core/src/lib/interfaces/unified-caller.interface.ts
git add libs/libs-api/auth-core/src/lib/interfaces/__tests__/unified-caller.spec.ts
git add libs/libs-api/auth-core/src/index.ts
git commit -m "[S227.1] feat: add UnifiedCaller interface and factory functions"

Task 2: Custom Decorators

Files:

Each uses NestJS SetMetadata:

Create libs/libs-api/auth-core/src/lib/decorators/__tests__/decorators.spec.ts:

Run: yarn nx test auth-core --testPathPattern=decorators Expected: PASS

Add exports to libs/libs-api/auth-core/src/index.ts

git add libs/libs-api/auth-core/src/lib/decorators/caller-type.decorator.ts
git add libs/libs-api/auth-core/src/lib/decorators/service-only.decorator.ts
git add libs/libs-api/auth-core/src/lib/decorators/tenant-scoped.decorator.ts
git add libs/libs-api/auth-core/src/lib/decorators/owner-only.decorator.ts
git add libs/libs-api/auth-core/src/lib/decorators/__tests__/decorators.spec.ts
git add libs/libs-api/auth-core/src/index.ts
git commit -m "[S227.1] feat: add CallerType, ServiceOnly, TenantScoped, OwnerOnly decorators"

Task 3: UnifiedAuthGuard Implementation

Files:

Reference: Read libs/libs-api/auth-core/src/lib/guards/jwt-auth.guard.ts first — the unified guard absorbs its logic.

Create libs/libs-api/auth-core/src/lib/guards/__tests__/unified-auth.guard.spec.ts with test suites:

Use createMockContext() helper that builds an ExecutionContext with configurable headers, query, ip.

Run: yarn nx test auth-core --testPathPattern=unified-auth Expected: FAIL — Cannot find module '../unified-auth.guard'

Create libs/libs-api/auth-core/src/lib/guards/unified-auth.guard.ts:

Run: yarn nx test auth-core --testPathPattern=unified-auth Expected: PASS — all tests

git add libs/libs-api/auth-core/src/lib/guards/unified-auth.guard.ts
git add libs/libs-api/auth-core/src/lib/guards/__tests__/unified-auth.guard.spec.ts
git add libs/libs-api/auth-core/src/index.ts
git commit -m "[S227.1] feat: implement UnifiedAuthGuard with 4-strategy auth chain"

Task 4: Register APP_GUARD (both UnifiedAuthGuard + RolesGuard)

Files:

IMPORTANT: This task uses the additive-then-subtractive strategy. Tasks 6-9 must add @Public() to all currently-unguarded controllers BEFORE this task commits. See Task ordering note below.

Reference: Read libs/libs-api/auth-core/src/lib/auth-core.module.ts before editing.

TASK ORDERING: Tasks 6-9 Step 2 (adding @Public() to intentionally-public routes) should be done BEFORE this step. This prevents a regression window where all 4 backends break simultaneously. The sequence is:

  1. Tasks 6-9: Add @Public() to ALL currently-unguarded controllers (safe — no-op without global guard)
  2. Task 4: Register APP_GUARD (now safe — all routes that need to be public already are)
  3. Tasks 6-9 continued: Remove @Public() from controllers that should now be protected

Import APP_GUARD from @nestjs/core and UnifiedAuthGuard. Add TWO global guards to the forRoot() providers array:

{ provide: APP_GUARD, useClass: UnifiedAuthGuard },  // Auth: deny-by-default
{ provide: APP_GUARD, useClass: RolesGuard },         // Roles: enforced globally

With RolesGuard as APP_GUARD, controllers no longer need @UseGuards(RolesGuard) — just @Roles() is sufficient. This eliminates the need to re-add @UseGuards(RolesGuard) to individual controllers after removing @UseGuards(JwtAuthGuard, RolesGuard).

Note: RolesGuard already returns true when no @Roles() metadata is present (line 70 of roles.guard.ts), so registering it globally is safe — routes without @Roles() are unaffected.

Check the terminal running yarn serve:backend-ai. Restart if needed. Verify health endpoint:

curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/ai-api/health

Expected: 200

curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/ai-api/dev/stats

Expected: 401 (previously protected by @UseGuards(JwtAuthGuard, RolesGuard), now double-guarded — redundant but harmless)

git add libs/libs-api/auth-core/src/lib/auth-core.module.ts
git commit -m "[S227.1] feat: register UnifiedAuthGuard as APP_GUARD in AuthCoreModule"

Task 5: Verify APP_GUARD on all backends

Files:

Reference: Read each backend's app.module.ts to verify AuthCoreModule.forRoot() is imported.

Check imports in (note: backend-api uses BackendAuthModule at APIs/backend-api/src/app/backend-auth/backend-auth.module.ts, not app.module.ts):

If backend-docs-api does NOT import AuthCoreModule, add it.

curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/health
curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/ai-api/health
curl -s -o /dev/null -w "%{http_code}" http://localhost:3002/docs-api/health
curl -s -o /dev/null -w "%{http_code}" http://localhost:3003/vitto-api/health

Expected: All 200. If any return 401, add @Public() to that backend's health endpoint.

git commit -m "[S227.1] feat: verify APP_GUARD active on all 4 backends"

Chunk 2: @Public() Audit + Guard Removal

Task 6: @Public() audit — backend-api (67 controllers)

Files:

Search for UseGuards(BackendAuthGuard in APIs/backend-api/src/. For each, remove the @UseGuards(BackendAuthGuard) decorator and its import. APP_GUARD handles authentication. Also remove @UseGuards(JwtAuthGuard) where used. Do NOT remove @UseGuards(ThrottlerGuard).

Add @Public() to class level on: BackendAuthController, PhoneAuthController, MagicLinkController, StripeWebhookController, AiAgentsMcpController, AiAgentsPublicController, MockRuntimeController.

These should NOT get @Public(): BenchmarkController, StudioGitController, DraftsController, PulseController, GalaxiesController, UniversalModulesController, ModuleExportController, TestModulesController, PlatformCodeController.

Read APIs/backend-vitto-api/src/app/engine/whistle-orchestrator.service.ts. The checkSpaceMembership() method calls GET /api/spaces/:spaceId/members without auth headers. Replace with BackendApiClientService call which already sends BACKEND_API_INTERNAL_TOKEN.

curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/benchmark  # Should be 401
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/health     # Should be 200
git add APIs/backend-api/src/app/
git add APIs/backend-vitto-api/src/app/engine/whistle-orchestrator.service.ts
git commit -m "[S227.2] refactor: remove per-controller guards from backend-api, add @Public() where needed"

Task 7: @Public() audit — backend-ai-api (62 controllers)

Files:

Same pattern as Task 6. Remove redundant per-controller @UseGuards(JwtAuthGuard).

For 12 infrastructure controllers: replace @UseGuards(InfrastructureAuthGuard) with @CallerType('service', 'dev').

UnifiedHealthController, ObservatoryController, McpJsonRpcController, PlatformKnowledgeController — add @Public() to class.

Find 4 methods with // Temporarily public for development comments. Remove both comment and @Public().

Read APIs/backend-ai-api/src/app/infrastructure/guards/computer-use-permission.guard.ts. Check what fields it reads from request.user. Update to use UnifiedCaller field names (userId, roles, callerType). This guard is KEPT — it's a permission guard, not an auth guard.

Read APIs/backend-ai-api/src/app/mcp-gateway/guards/tier-limit.guard.ts. It reads user.spaceId and user.mcpTier. Verify these fields exist in UnifiedCaller (they do — spaceId and mcpTier are both in the interface). Update any other field access as needed.

Delete infrastructure-auth.guard.ts, infrastructure-auth.guard.spec.ts, project-api-key.guard.ts. Do NOT delete computer-use-permission.guard.ts or tier-limit.guard.ts. Update module imports that referenced deleted guards.

yarn nx test backend-ai-api --passWithNoTests
git add APIs/backend-ai-api/src/
git commit -m "[S227.2] refactor: remove per-controller guards from backend-ai-api, delete deprecated guards"

Task 8: @Public() audit — backend-vitto-api (21 controllers)

Files:

Controllers that should NOT get @Public() (now properly protected):

Controllers that SHOULD get @Public():

Remove ALL @UseGuards(JwtAuthGuard), @UseGuards(JwtAuthGuard, RolesGuard), and @UseGuards(JwtAuthGuard, SpaceOwnershipGuard) decorators entirely. Both UnifiedAuthGuard and RolesGuard are now APP_GUARD — no per-controller registration needed. Keep @Roles() decorators as-is — they work with the global RolesGuard.

For SpaceOwnershipGuard: keep it as @UseGuards(SpaceOwnershipGuard) (it's an authorization guard, not an auth guard).

SecurityController previously had @Roles('admin') without @UseGuards(JwtAuthGuard) — broken. Now that both guards are global, @Roles('admin') works automatically. No changes needed to this controller — just verify.

Add a comment at the top explaining ~114 @Public() annotations are for SSE endpoints. SSE ticket-based auth is planned for a future session.

git add APIs/backend-vitto-api/src/
git commit -m "[S227.2] refactor: remove per-controller guards from backend-vitto-api, fix SecurityController"

Task 9: Audit backend-docs-api + Delete BackendAuthGuard

Files:

Read all controllers. Add @Public() to health/webhook/sync endpoints. Leave others protected.

Remove APIs/backend-api/src/app/backend-auth/backend-auth.guard.ts. Remove from backend-auth.module.ts exports. Fix all remaining imports.

yarn nx build backend-api --skip-nx-cache
yarn nx build backend-ai-api --skip-nx-cache
yarn nx build backend-vitto-api --skip-nx-cache
yarn nx build backend-docs-api --skip-nx-cache
git add APIs/backend-api/src/app/backend-auth/
git add APIs/backend-docs-api/src/
git commit -m "[S227.2] refactor: delete BackendAuthGuard, audit backend-docs-api"

Chunk 3: Tenant Model + Internal Endpoints

Task 10: Tenant sync utility + schema changes

Files:

Create libs/libs-api/auth-core/src/lib/utils/tenant-sync.util.ts:

Test: ensureTenantSync copies spaceId→tenantId, tenantId→spaceId, handles both missing, doesn't overwrite when both exist.

Run: yarn nx test auth-core --testPathPattern=tenant-sync Expected: PASS

Read schema, add: @Prop({ type: String, index: true }) tenantId?: string; Add tenantSyncPlugin to the schema.

Read schema, add: @Prop({ type: String }) tenantId?: string; Add tenantSyncPlugin.

Read schema. It already has tenantId and spaceId. Add: ChatSessionSchema.plugin(tenantSyncPlugin);

Read APIs/backend-vitto-api/src/app/module-generation/checkpoint/generation-session.types.ts. Add tenantId?: string to the GenerationCheckpoint interface. If there's a Mongoose schema for checkpoints, add the field and plugin there too.

git add libs/libs-api/auth-core/src/lib/utils/
git add libs/libs-api/auth-core/src/index.ts
git add APIs/backend-vitto-api/src/app/mind/schemas/
git add APIs/backend-ai-api/src/app/sessions/sessions.schema.ts
git commit -m "[S227.3] feat: add tenantId to Vitto schemas, create tenant sync plugin"

Task 11: Add tenantId filtering to session list endpoints

Files:

Reference: Read both files before editing.

In controller: add @Query('tenantId') tenantId?: string to getMyActiveSession() and getMyHistory(). Pass to service methods.

In service: add optional tenantId parameter to getActiveSession() and getUserSessions(). Add to Mongoose query filter when provided.

Same pattern for getMyGroups() — add optional tenantId query param.

In APIs/backend-ai-api/src/app/workflow/, find the ConfigurationSessionController. Add required tenantId filter from JWT to GET /workflow/sessions. Add @TenantScoped() to GET /workflow/sessions/stats/:tenantId.

In upsertSession() and appendMessages(): call ensureTenantSync() on data before writing.

git add APIs/backend-ai-api/src/app/sessions/
git add APIs/backend-ai-api/src/app/workflow/
git commit -m "[S227.3] feat: add tenantId filtering to session list endpoints"

Task 12: Internal sessions controller (backend-ai-api)

Files:

Module imports ChatSessionsModule. Controller uses @ServiceOnly() at class level.

Endpoints:

Query: find({ tenantId, genome: { $exists: true, $ne: null } }), select key fields, limit 100.

Create APIs/backend-ai-api/src/app/internal/__tests__/internal-sessions.controller.spec.ts:

curl -H "Authorization: Bearer $SYSTEM_TOKEN" http://localhost:3001/ai-api/internal/sessions/tenant/test-space
# Expected: 200

curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/ai-api/internal/sessions/tenant/test-space
# Expected: 401
git add APIs/backend-ai-api/src/app/internal/
git add APIs/backend-ai-api/src/app/app.module.ts
git commit -m "[S227.4] feat: create internal sessions controller for service-to-service calls"

Task 13: Internal controllers (backend-api)

Files:

Both controllers use @ServiceOnly(). Endpoints:

git add APIs/backend-api/src/app/internal/
git add APIs/backend-api/src/app/app.module.ts
git commit -m "[S227.4] feat: create internal spaces/users controllers for backend-api"

Task 14: Update Vitto connectivity clients to use internal routes

Files:

Reference: Read both files first. These are 900+ line files — only change methods targeting cross-user endpoints.

Change /ai-api/sessions/user/:userId to /ai-api/internal/sessions/user/:userId. Change any cross-user /ai-api/sessions/:id to /ai-api/internal/sessions/:id.

Change /api/spaces/:spaceId/members to /api/internal/spaces/:spaceId/members. These methods already send BACKEND_API_INTERNAL_TOKEN.

git add APIs/backend-vitto-api/src/app/connectivity/
git commit -m "[S227.4] refactor: update Vitto connectivity clients to use /internal/* routes"

Chunk 4: Whistle Hardening + Session Ownership

Task 15: Remove @Public() from whistle endpoints

Files:

Replace @Public() with nothing (APP_GUARD protects it). Add @CurrentUser() parameter. Use user.userId instead of body.whistlerUserId.

This endpoint is only called by vitto-api. Add @ServiceOnly() decorator.

git add APIs/backend-vitto-api/src/app/engine/vitto.controller.ts
git add APIs/backend-ai-api/src/app/sessions/sessions.controller.ts
git commit -m "[S227.5] fix: remove @Public() from whistle endpoints, require JWT auth"

Task 16: Add whistler membership check + fix LLM whistle tool

Files:

In whistle() method, after target membership check: verify whistler is also a member. Return error if not.

Replace raw http.post()/http.get() calls with BackendApiClientService and BackendAiClientService.

In whistle.plugin.ts: add member roster lookup to resolve targetName to targetUserId via backendApiClient.getSpaceMembers(spaceId).

Read APIs/backend-vitto-api/src/app/mind/schemas/user-notification-prefs.schema.ts. Change whistlePreferences.allowlist from string[] to an array of { userId: string, spaceId?: string }. When spaceId is set, only allow whistle from that specific space. When null, allow from any shared space.

Update WhistleOrchestratorService.checkWhistlePreferences() to pass spaceId into the allowlist check.

git add APIs/backend-vitto-api/src/app/engine/whistle-orchestrator.service.ts
git add APIs/backend-vitto-api/src/app/tools/plugins/whistle.plugin.ts
git add APIs/backend-vitto-api/src/app/mind/schemas/user-notification-prefs.schema.ts
git commit -m "[S227.5] fix: add whistler membership check, space-scope allowlist, fix LLM whistle"

Task 17: Add ownership checks to session endpoints

Files:

Reference: Read the controller before editing.

After fetching session: if user.callerType === 'service', allow. Otherwise verify session.userId === user.userId OR session.participants?.some(p => p.userId === user.userId). Return { session: null } if unauthorized (prevents enumeration).

Same pattern. Only owner can delete.

Same owner-or-participant check. Service callers bypass.

Add @ServiceOnly() or deprecate. Frontend uses /my/history.

git add APIs/backend-ai-api/src/app/sessions/sessions.controller.ts
git commit -m "[S227.6] fix: add ownership verification to all session-by-ID endpoints"

Chunk 5: WebSocket Security + Cleanup

Task 18: WebSocket auth guard

Files:

@Injectable() utility (not a NestJS guard — APP_GUARD doesn't apply to WS). Used in handleConnection():

Test: JWT in auth object, system token auth, invalid token returns null.

Run: yarn nx test auth-core --testPathPattern=ws-auth Expected: PASS

In chat.gateway.ts handleConnection(): inject WsAuthGuard, call authenticate(client), disconnect if null, set client.data.user.

git add libs/libs-api/auth-core/src/lib/guards/ws-auth.guard.ts
git add libs/libs-api/auth-core/src/lib/guards/__tests__/ws-auth.guard.spec.ts
git add libs/libs-api/auth-core/src/index.ts
git add APIs/backend-ai-api/src/app/sessions/chat.gateway.ts
git commit -m "[S227.7] feat: create WsAuthGuard, apply to ChatGateway"

Task 19: Endpoint consolidation

Files:

Add @ServiceOnly() to: GET /sessions/user/:userId, GET /sessions/user/:userId/genomes, DELETE /sessions/cleanup/old.

Mark POST /infrastructure/sessions/create and GET /infrastructure/sessions/list as deprecated (add comment, keep functional).

In APIs/backend-api/src/app/ai-agents/ai-agents.controller.ts, mark as deprecated:

Add deprecation comments. Consider adding @ServiceOnly() to restrict access to dev tools only.

git add APIs/backend-ai-api/src/app/sessions/sessions.controller.ts
git add APIs/backend-ai-api/src/app/infrastructure/controllers/infrastructure-sessions.controller.ts
git commit -m "[S227.8] refactor: consolidate duplicate endpoints, restrict cross-user queries"

Task 20: Token consolidation

Files:

Replace native fetch() call with proper headers including Authorization: Bearer ${BACKEND_API_INTERNAL_TOKEN}.

Change from reading SYSTEM_TOKEN to BACKEND_API_INTERNAL_TOKEN for outbound calls.

In AppService.onModuleInit() for each backend: log error if required token env vars are missing.

git add APIs/backend-ai-api/src/app/mcp-gateway/services/mcp-usage.service.ts
git add APIs/backend-ai-api/src/app/workflow/services/platform-orchestration.service.ts
git commit -m "[S227.9] fix: token consolidation and startup validation"

Task 21: Data migration script

Files:

Script connects to ai-agents and vitto-mind MongoDB databases. For each of chat_sessions, configuration_sessions, agent-sessions, customer-sessions:

npx ts-node scripts/migrations/backfill-tenant-id.ts
git add scripts/migrations/backfill-tenant-id.ts
git commit -m "[S227.3] feat: add tenantId backfill migration script"

Task 22: Final verification

yarn nx test auth-core

Expected: All pass

yarn nx build backend-api --skip-nx-cache
yarn nx build backend-ai-api --skip-nx-cache
yarn nx build backend-vitto-api --skip-nx-cache
yarn nx build backend-docs-api --skip-nx-cache

Expected: All build successfully

Health endpoints (public) → 200. Protected endpoints without token → 401. Internal endpoints with system token → 200. Internal endpoints without token → 401.

Call outcome() and intelligence({ action: "reflect" }) with task summary and lessons learned.


Summary

Phase Tasks Sub-sessions Scope
Foundation 1-5 S227.1 UnifiedCaller, decorators, UnifiedAuthGuard, dual APP_GUARD registration
Guard Audit 6-9 S227.2 Remove per-controller guards, add @Public(), delete deprecated guards, audit ComputerUsePermissionGuard/TierLimitGuard
Tenant Model 10-14 S227.3-4 tenantId schema changes (including GenerationCheckpoint), internal endpoints with tests, connectivity client updates
Hardening 15-17 S227.5-6 Whistle auth + space-scoped allowlist, LLM tool fix, session ownership checks
WS + Cleanup 18-22 S227.7-9 WebSocket auth (ChatGateway), endpoint consolidation + MCP deprecation, token cleanup, migration

Total: 22 tasks, ~9 sub-sessions, targeting S227 branch

Rollout order (critical): Tasks 6-9 add @Public() FIRST → Task 4 registers APP_GUARD → Tasks 6-9 remove @Public() from protected controllers

Rollback: Revert APP_GUARD registration in auth-core.module.ts (1 line). Re-add per-controller @UseGuards() from git history. @Public() annotations and internal endpoints are harmless without a global guard.