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:
- SSE ticket-based auth (spec Part 1.7) — requires frontend changes, low risk since SSE endpoints are read-only
- 11 additional internal endpoints (spec Part 2.3) —
POST .../messages,GET .../search, workflow/embedding/billing/vitto internal routes. Core 7 endpoints cover the critical Vitto use cases. - Frontend
SessionInfo.tenantIdpopulation — backend-first approach - Full WebSocket gateway audit for 7 remaining gateways beyond ChatGateway — ChatGateway is the only user-facing one; others are dev/service tooling
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.tsModify:
libs/libs-api/auth-core/src/index.tsTest:
libs/libs-api/auth-core/src/lib/interfaces/__tests__/unified-caller.spec.tsStep 1: Write the interface and factory functions
Create libs/libs-api/auth-core/src/lib/interfaces/unified-caller.interface.ts with:
CallerTypeValuetype:'user' | 'service' | 'api-key' | 'dev'AuthMethodValuetype:'jwt' | 'service-jwt' | 'system-token' | 'api-key' | 'localhost'UnifiedCallerinterface with fields:userId,sub(both set to same value),email?,displayName?,callerType,serviceName?,roles,platformRole?,permissions,scopes,tenantId?,spaceId?,authMethod,isApiKeyAuth,isLocalhostBypass,mcpTier?,userType?createUserCaller(payload)— maps JWT payload to UnifiedCaller, syncstenantId/spaceIdcreateServiceCaller(serviceName)— createssvc:{name}caller with system rolecreateApiKeyCaller(ownerId, projectId, tier?)— createsapikey:{ownerId}callercreateDevCaller()— creates localhost-dev callerStep 2: Write tests for factory functions
Create libs/libs-api/auth-core/src/lib/interfaces/__tests__/unified-caller.spec.ts:
createUserCallersetsuserIdandsubto same value frompayload.subcreateUserCallersyncstenantIdfromspaceIdand vice versacreateUserCallerbuildsroles[]from singlerolestringcreateServiceCallerprefixesuserIdwithsvc:createApiKeyCallersetsisApiKeyAuth: truecreateDevCallersetsisLocalhostBypass: trueStep 3: Run tests
Run: yarn nx test auth-core --testPathPattern=unified-caller
Expected: PASS — all 6 tests
- Step 4: Export from barrel
Add exports to libs/libs-api/auth-core/src/index.ts:
UnifiedCaller, CallerTypeValue, AuthMethodValue, createUserCaller, createServiceCaller, createApiKeyCaller, createDevCaller
- Step 5: Commit
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:
Create:
libs/libs-api/auth-core/src/lib/decorators/caller-type.decorator.tsCreate:
libs/libs-api/auth-core/src/lib/decorators/service-only.decorator.tsCreate:
libs/libs-api/auth-core/src/lib/decorators/tenant-scoped.decorator.tsCreate:
libs/libs-api/auth-core/src/lib/decorators/owner-only.decorator.tsTest:
libs/libs-api/auth-core/src/lib/decorators/__tests__/decorators.spec.tsModify:
libs/libs-api/auth-core/src/index.tsStep 1: Create all 4 decorator files
Each uses NestJS SetMetadata:
@CallerType(...types: CallerTypeValue[])— setsCALLER_TYPE_KEYmetadata@ServiceOnly()— shorthand for@CallerType('service')@TenantScoped()— setsTENANT_SCOPED_KEYmetadata totrue@OwnerOnly()— setsOWNER_ONLY_KEYmetadata totrueStep 2: Write tests for decorators
Create libs/libs-api/auth-core/src/lib/decorators/__tests__/decorators.spec.ts:
@CallerType('user', 'service')sets expected metadata array@ServiceOnly()sets['service']metadata@TenantScoped()setstruemetadata@OwnerOnly()setstruemetadataStep 3: Run tests
Run: yarn nx test auth-core --testPathPattern=decorators
Expected: PASS
- Step 4: Export from barrel
Add exports to libs/libs-api/auth-core/src/index.ts
- Step 5: Commit
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:
- Create:
libs/libs-api/auth-core/src/lib/guards/unified-auth.guard.ts - Test:
libs/libs-api/auth-core/src/lib/guards/__tests__/unified-auth.guard.spec.ts - Modify:
libs/libs-api/auth-core/src/index.ts
Reference: Read libs/libs-api/auth-core/src/lib/guards/jwt-auth.guard.ts first — the unified guard absorbs its logic.
- Step 1: Write failing tests (core scenarios)
Create libs/libs-api/auth-core/src/lib/guards/__tests__/unified-auth.guard.spec.ts with test suites:
- Public routes: allows access without token; still decodes JWT on public routes (soft-auth)
- System token auth: authenticates with SYSTEM_TOKEN, sets
callerType: 'service' - JWT auth: authenticates valid JWT, sets
userIdandsubto same value, syncs tenantId/spaceId - JWT failure: throws
UnauthorizedExceptionfor invalid JWT on non-public route - Localhost bypass: allows localhost in development, sets
callerType: 'dev' - No auth: throws
UnauthorizedExceptionwhen no token on non-public route (production mode) - CallerType enforcement: rejects service caller on
@CallerType('user')route; allows user caller
Use createMockContext() helper that builds an ExecutionContext with configurable headers, query, ip.
- Step 2: Run tests to verify they fail
Run: yarn nx test auth-core --testPathPattern=unified-auth
Expected: FAIL — Cannot find module '../unified-auth.guard'
- Step 3: Implement UnifiedAuthGuard
Create libs/libs-api/auth-core/src/lib/guards/unified-auth.guard.ts:
@Injectable()class implementingCanActivateConstructor injects
Reflector,JwtService,ConfigService,AUTH_CORE_OPTIONScanActivate()tries auth strategies in order:- Check
@Public()metadata - Extract token from
Authorization: Bearerheader or?token=query param - Try system token match →
createServiceCaller('system') - Try JWT verify →
createUserCaller(payload) - Try API key header (
X-MCP-API-Key) →createApiKeyCaller() - Try localhost bypass (dev only) →
createDevCaller() - If public → return true (soft-auth)
- If no caller → throw
UnauthorizedException - Check
@CallerType()metadata → throwForbiddenExceptionif mismatch - Return
true
- Check
Step 4: Run tests to verify they pass
Run: yarn nx test auth-core --testPathPattern=unified-auth
Expected: PASS — all tests
- Step 5: Export from barrel and commit
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:
- Modify:
libs/libs-api/auth-core/src/lib/auth-core.module.ts
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:
- Tasks 6-9: Add
@Public()to ALL currently-unguarded controllers (safe — no-op without global guard) - Task 4: Register APP_GUARD (now safe — all routes that need to be public already are)
- Tasks 6-9 continued: Remove
@Public()from controllers that should now be protected
- Step 1: Add dual APP_GUARD registration to AuthCoreModule.forRoot()
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.
- Step 2: Verify backend-ai-api starts without errors
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
- Step 3: Test a protected endpoint without token
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)
- Step 4: Commit
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:
- Possibly modify:
APIs/backend-docs-api/src/app/app.module.ts(if missing AuthCoreModule)
Reference: Read each backend's app.module.ts to verify AuthCoreModule.forRoot() is imported.
- Step 1: Verify all 4 backends import AuthCoreModule
Check imports in (note: backend-api uses BackendAuthModule at APIs/backend-api/src/app/backend-auth/backend-auth.module.ts, not app.module.ts):
APIs/backend-api/src/app/backend-auth/backend-auth.module.ts(containsAuthCoreModule.forRoot())APIs/backend-ai-api/src/app/app.module.tsAPIs/backend-vitto-api/src/app/app.module.tsAPIs/backend-docs-api/src/app/app.module.ts
If backend-docs-api does NOT import AuthCoreModule, add it.
- Step 2: Verify health endpoints work on all backends
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.
- Step 3: Commit if changes needed
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:
Modify: ~44 controllers in
APIs/backend-api/src/app/(remove@UseGuards)Modify: ~23 controllers in
APIs/backend-api/src/app/(add@Public()where intentionally public)Modify:
APIs/backend-vitto-api/src/app/engine/whistle-orchestrator.service.ts(fix auth headers — CRITICAL)Step 1: Remove @UseGuards(BackendAuthGuard) from all 44 guarded controllers
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).
- Step 2: Add @Public() to intentionally public routes
Add @Public() to class level on: BackendAuthController, PhoneAuthController, MagicLinkController, StripeWebhookController, AiAgentsMcpController, AiAgentsPublicController, MockRuntimeController.
- Step 3: Verify previously-unguarded controllers are now protected
These should NOT get @Public(): BenchmarkController, StudioGitController, DraftsController, PulseController, GalaxiesController, UniversalModulesController, ModuleExportController, TestModulesController, PlatformCodeController.
- Step 4: Fix WhistleOrchestratorService auth headers (CRITICAL)
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.
- Step 5: Restart backend-api and smoke test
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
- Step 6: Commit
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:
Modify: ~25 controllers (remove
@UseGuards(JwtAuthGuard))Modify: ~12 infrastructure controllers (replace
@UseGuards(InfrastructureAuthGuard)with@CallerType('service', 'dev'))Delete:
APIs/backend-ai-api/src/app/infrastructure/guards/infrastructure-auth.guard.tsDelete:
APIs/backend-ai-api/src/app/infrastructure/guards/infrastructure-auth.guard.spec.tsDelete:
APIs/backend-ai-api/src/app/mcp-gateway/guards/project-api-key.guard.tsStep 1: Remove @UseGuards(JwtAuthGuard) from 25 guarded controllers
Same pattern as Task 6. Remove redundant per-controller @UseGuards(JwtAuthGuard).
- Step 2: Replace InfrastructureAuthGuard with CallerType decorator
For 12 infrastructure controllers: replace @UseGuards(InfrastructureAuthGuard) with @CallerType('service', 'dev').
- Step 3: Add @Public() to intentionally public routes
UnifiedHealthController, ObservatoryController, McpJsonRpcController, PlatformKnowledgeController — add @Public() to class.
- Step 4: Remove "Temporarily public" @Public() from PlatformAIController
Find 4 methods with // Temporarily public for development comments. Remove both comment and @Public().
- Step 5: Audit ComputerUsePermissionGuard for UnifiedCaller compatibility
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.
- Step 6: Audit TierLimitGuard for UnifiedCaller compatibility
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.
- Step 7: Delete old guard files
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.
- Step 8: Verify and commit
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:
- Modify: ~11 controllers (remove
@UseGuards) - Verify:
APIs/backend-vitto-api/src/app/security/security.controller.ts(broken @Roles now auto-fixed by global RolesGuard)
Controllers that should NOT get @Public() (now properly protected):
ConnectorController— API keys/secrets managementTrainingController— Agent training operationsPlatformGenerationController— Full generation pipelineStudioProjectController— Studio project management
Controllers that SHOULD get @Public():
CustomerController— customer-facing (already@Public())AwarenessController— observatory SSE (already@Public())McpServerController— MCP protocol (already@Public())A2AControllerdiscovery endpoint —GET .well-known/agent.json(add@Public()to that method only)Step 1: Remove @UseGuards from 11 guarded controllers
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).
- Step 2: Verify SecurityController @Roles() now works
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.
- Step 3: Document vitto.controller.ts @Public() status
Add a comment at the top explaining ~114 @Public() annotations are for SSE endpoints. SSE ticket-based auth is planned for a future session.
- Step 4: Commit
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:
Modify: Controllers in
APIs/backend-docs-api/src/app/Delete:
APIs/backend-api/src/app/backend-auth/backend-auth.guard.tsModify:
APIs/backend-api/src/app/backend-auth/backend-auth.module.tsStep 1: Audit backend-docs-api controllers
Read all controllers. Add @Public() to health/webhook/sync endpoints. Leave others protected.
- Step 2: Delete BackendAuthGuard
Remove APIs/backend-api/src/app/backend-auth/backend-auth.guard.ts. Remove from backend-auth.module.ts exports. Fix all remaining imports.
- Step 3: Verify all 4 backends build
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
- Step 4: Commit
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.tsTest:
libs/libs-api/auth-core/src/lib/utils/__tests__/tenant-sync.spec.tsModify:
APIs/backend-vitto-api/src/app/mind/schemas/agent-session.schema.ts(add tenantId)Modify:
APIs/backend-vitto-api/src/app/mind/schemas/customer-session.schema.ts(add tenantId)Modify:
APIs/backend-ai-api/src/app/sessions/sessions.schema.ts(add tenantSyncPlugin)Modify:
libs/libs-api/auth-core/src/index.tsStep 1: Create tenant sync utility
Create libs/libs-api/auth-core/src/lib/utils/tenant-sync.util.ts:
ensureTenantSync(data)— ifspaceIdset but nottenantId, copy; and vice versatenantSyncPlugin(schema)— Mongoose plugin withpre('save')andpre(['findOneAndUpdate', 'updateMany', 'updateOne'])hooksStep 2: Write tests
Test: ensureTenantSync copies spaceId→tenantId, tenantId→spaceId, handles both missing, doesn't overwrite when both exist.
- Step 3: Run tests
Run: yarn nx test auth-core --testPathPattern=tenant-sync
Expected: PASS
- Step 4: Add tenantId to AgentSessionDoc
Read schema, add: @Prop({ type: String, index: true }) tenantId?: string;
Add tenantSyncPlugin to the schema.
- Step 5: Add tenantId to CustomerSession
Read schema, add: @Prop({ type: String }) tenantId?: string;
Add tenantSyncPlugin.
- Step 6: Add tenantSyncPlugin to ChatSession schema
Read schema. It already has tenantId and spaceId. Add: ChatSessionSchema.plugin(tenantSyncPlugin);
- Step 7: Add tenantId to GenerationCheckpoint types
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.
- Step 8: Export and commit
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:
- Modify:
APIs/backend-ai-api/src/app/sessions/sessions.controller.ts - Modify:
APIs/backend-ai-api/src/app/sessions/sessions.service.ts
Reference: Read both files before editing.
- Step 1: Add optional tenantId query param to /my/active and /my/history
In controller: add @Query('tenantId') tenantId?: string to getMyActiveSession() and getMyHistory(). Pass to service methods.
- Step 2: Update service methods to filter by tenantId
In service: add optional tenantId parameter to getActiveSession() and getUserSessions(). Add to Mongoose query filter when provided.
- Step 3: Add tenantId filter to /my/groups
Same pattern for getMyGroups() — add optional tenantId query param.
- Step 4: Add tenantId filter to workflow sessions
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.
- Step 5: Ensure tenantId is set on session write paths
In upsertSession() and appendMessages(): call ensureTenantSync() on data before writing.
- Step 6: Commit
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:
Create:
APIs/backend-ai-api/src/app/internal/internal.module.tsCreate:
APIs/backend-ai-api/src/app/internal/internal-sessions.controller.tsModify:
APIs/backend-ai-api/src/app/app.module.tsPossibly modify:
APIs/backend-ai-api/src/app/sessions/sessions.service.ts(addgetGenomesByTenant)Step 1: Create InternalModule and InternalSessionsController
Module imports ChatSessionsModule. Controller uses @ServiceOnly() at class level.
Endpoints:
GET internal/sessions/tenant/:tenantId— sessions by tenantGET internal/sessions/tenant/:tenantId/genomes— genomes by tenantGET internal/sessions/user/:userId— sessions by user (cross-tenant)GET internal/sessions/:id— any session by ID (no ownership check)Step 2: Add getGenomesByTenant to service if missing
Query: find({ tenantId, genome: { $exists: true, $ne: null } }), select key fields, limit 100.
Step 3: Register InternalModule in app.module.ts
Step 4: Write test for @ServiceOnly() enforcement
Create APIs/backend-ai-api/src/app/internal/__tests__/internal-sessions.controller.spec.ts:
Test that a request with
callerType: 'user'receives 403Test that a request with
callerType: 'service'(system token) receives 200Test tenant/:tenantId endpoint returns sessions filtered by tenantId
Step 5: Test internal endpoint (manual smoke test)
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
- Step 6: Commit
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:
Create:
APIs/backend-api/src/app/internal/internal.module.tsCreate:
APIs/backend-api/src/app/internal/internal-spaces.controller.tsCreate:
APIs/backend-api/src/app/internal/internal-users.controller.tsModify:
APIs/backend-api/src/app/app.module.tsStep 1: Create InternalModule, InternalSpacesController, InternalUsersController
Both controllers use @ServiceOnly(). Endpoints:
GET internal/spaces/:spaceId— space detailsGET internal/spaces/:spaceId/members— member listGET internal/users/:userId— user profileStep 2: Register in app.module.ts
Step 3: Commit
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:
- Modify:
APIs/backend-vitto-api/src/app/connectivity/backend-ai-client.service.ts - Modify:
APIs/backend-vitto-api/src/app/connectivity/backend-api-client.service.ts
Reference: Read both files first. These are 900+ line files — only change methods targeting cross-user endpoints.
- Step 1: Update session endpoint URLs in BackendAiClientService
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.
- Step 2: Update space/user endpoint URLs in BackendApiClientService
Change /api/spaces/:spaceId/members to /api/internal/spaces/:spaceId/members. These methods already send BACKEND_API_INTERNAL_TOKEN.
- Step 3: Verify and commit
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:
Modify:
APIs/backend-vitto-api/src/app/engine/vitto.controller.ts(~line 4011)Modify:
APIs/backend-ai-api/src/app/sessions/sessions.controller.tsStep 1: Remove @Public() from POST /engine/whistle
Replace @Public() with nothing (APP_GUARD protects it). Add @CurrentUser() parameter. Use user.userId instead of body.whistlerUserId.
- Step 2: Move POST /sessions/:id/whistle to @ServiceOnly()
This endpoint is only called by vitto-api. Add @ServiceOnly() decorator.
- Step 3: Commit
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:
Modify:
APIs/backend-vitto-api/src/app/engine/whistle-orchestrator.service.tsModify:
APIs/backend-vitto-api/src/app/tools/plugins/whistle.plugin.tsStep 1: Add whistler membership verification
In whistle() method, after target membership check: verify whistler is also a member. Return error if not.
- Step 2: Route through typed clients
Replace raw http.post()/http.get() calls with BackendApiClientService and BackendAiClientService.
- Step 3: Fix LLM whistle tool userId resolution
In whistle.plugin.ts: add member roster lookup to resolve targetName to targetUserId via backendApiClient.getSpaceMembers(spaceId).
- Step 4: Space-scope the whistle allowlist
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.
- Step 5: Commit
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:
- Modify:
APIs/backend-ai-api/src/app/sessions/sessions.controller.ts
Reference: Read the controller before editing.
- Step 1: Add ownership check to GET /sessions/:id
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).
- Step 2: Add ownership check to DELETE /sessions/:id
Same pattern. Only owner can delete.
- Step 3: Add participant check to POST /sessions/:id/messages, reconcile, genome, awareness-message
Same owner-or-participant check. Service callers bypass.
- Step 4: Restrict GET /sessions/history
Add @ServiceOnly() or deprecate. Frontend uses /my/history.
- Step 5: Commit
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:
Create:
libs/libs-api/auth-core/src/lib/guards/ws-auth.guard.tsTest:
libs/libs-api/auth-core/src/lib/guards/__tests__/ws-auth.guard.spec.tsModify:
APIs/backend-ai-api/src/app/sessions/chat.gateway.tsModify:
libs/libs-api/auth-core/src/index.tsStep 1: Create WsAuthGuard
@Injectable() utility (not a NestJS guard — APP_GUARD doesn't apply to WS). Used in handleConnection():
Extracts token from
client.handshake.auth.token,query.token, orheaders.authorizationTries system token →
createServiceCallerTries JWT verify →
createUserCallerTries localhost bypass (dev only) →
createDevCallerReturns
UnifiedCaller | nullStep 2: Write tests
Test: JWT in auth object, system token auth, invalid token returns null.
- Step 3: Run tests
Run: yarn nx test auth-core --testPathPattern=ws-auth
Expected: PASS
- Step 4: Apply to ChatGateway
In chat.gateway.ts handleConnection(): inject WsAuthGuard, call authenticate(client), disconnect if null, set client.data.user.
- Step 5: Export and commit
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:
Modify:
APIs/backend-ai-api/src/app/sessions/sessions.controller.tsModify:
APIs/backend-ai-api/src/app/infrastructure/controllers/infrastructure-sessions.controller.tsStep 1: Restrict cross-user endpoints
Add @ServiceOnly() to: GET /sessions/user/:userId, GET /sessions/user/:userId/genomes, DELETE /sessions/cleanup/old.
- Step 2: Deprecate duplicate infrastructure endpoints
Mark POST /infrastructure/sessions/create and GET /infrastructure/sessions/list as deprecated (add comment, keep functional).
- Step 3: Deprecate backend-api MCP migration endpoints
In APIs/backend-api/src/app/ai-agents/ai-agents.controller.ts, mark as deprecated:
GET /ai-agents/sessions/mongo-summary— debugging toolPOST /ai-agents/sessions/pull-from-mongo— one-time migrationPOST /ai-agents/sessions/sync-to-mongo— one-time migration
Add deprecation comments. Consider adding @ServiceOnly() to restrict access to dev tools only.
- Step 4: Commit
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:
Modify:
APIs/backend-ai-api/src/app/mcp-gateway/services/mcp-usage.service.tsModify:
APIs/backend-ai-api/src/app/workflow/services/platform-orchestration.service.tsStep 1: Fix McpUsageService to use Bearer token
Replace native fetch() call with proper headers including Authorization: Bearer ${BACKEND_API_INTERNAL_TOKEN}.
- Step 2: Fix PlatformOrchestrationService token variable
Change from reading SYSTEM_TOKEN to BACKEND_API_INTERNAL_TOKEN for outbound calls.
- Step 3: Add startup token validation warnings
In AppService.onModuleInit() for each backend: log error if required token env vars are missing.
- Step 4: Commit
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:
Create:
scripts/migrations/backfill-tenant-id.tsStep 1: Write migration script
Script connects to ai-agents and vitto-mind MongoDB databases. For each of chat_sessions, configuration_sessions, agent-sessions, customer-sessions:
Backfill
tenantIdfromspaceIdwhere missingBackfill
spaceIdfromtenantIdwhere missingFlag sessions without
userIdwith_needsReview: true(setuserId: null,tenantId: null)Step 2: Run the migration
npx ts-node scripts/migrations/backfill-tenant-id.ts
- Step 3: Commit
git add scripts/migrations/backfill-tenant-id.ts
git commit -m "[S227.3] feat: add tenantId backfill migration script"
Task 22: Final verification
- Step 1: Run all auth-core tests
yarn nx test auth-core
Expected: All pass
- Step 2: Build all 4 backends
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
- Step 3: Smoke test critical paths
Health endpoints (public) → 200. Protected endpoints without token → 401. Internal endpoints with system token → 200. Internal endpoints without token → 401.
- Step 4: Log outcome to MCP intelligence
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.