From f7b95f60666a4abbbad9a08669637b19fa87c430 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 13 Jul 2026 07:58:06 -0500 Subject: [PATCH] feat(gateway): expose Mos coordination boundary --- apps/gateway/src/coord/coord.module.ts | 3 +- .../coord/mos-coordination.controller.test.ts | 36 +++++++++ .../src/coord/mos-coordination.controller.ts | 73 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 apps/gateway/src/coord/mos-coordination.controller.test.ts create mode 100644 apps/gateway/src/coord/mos-coordination.controller.ts diff --git a/apps/gateway/src/coord/coord.module.ts b/apps/gateway/src/coord/coord.module.ts index 774cf13..170280d 100644 --- a/apps/gateway/src/coord/coord.module.ts +++ b/apps/gateway/src/coord/coord.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { InMemoryMosCoordinationPort } from '@mosaicstack/coord'; import { CoordService } from './coord.service.js'; import { CoordController } from './coord.controller.js'; +import { MosCoordinationController } from './mos-coordination.controller.js'; import { MOS_COORDINATION_CONFIG, MOS_COORDINATION_PORT, @@ -24,7 +25,7 @@ import { }, MosCoordinationService, ], - controllers: [CoordController], + controllers: [CoordController, MosCoordinationController], exports: [CoordService, MosCoordinationService], }) export class CoordModule {} diff --git a/apps/gateway/src/coord/mos-coordination.controller.test.ts b/apps/gateway/src/coord/mos-coordination.controller.test.ts new file mode 100644 index 0000000..0bec4b7 --- /dev/null +++ b/apps/gateway/src/coord/mos-coordination.controller.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from 'vitest'; +import { MosCoordinationController } from './mos-coordination.controller.js'; + +const user = { id: 'operator-1', tenantId: 'tenant-a' }; + +describe('MosCoordinationController', () => { + it('derives actor and tenant from the authenticated user rather than handoff input', async () => { + const coordination = { + handoff: vi.fn(async () => ({ handoffId: 'handoff-1' })), + observe: vi.fn(), + result: vi.fn(), + }; + const controller = new MosCoordinationController(coordination as never); + + await controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, 'corr-1'); + + expect(coordination.handoff).toHaveBeenCalledWith( + { idempotencyKey: 'request-1', summary: 'Implement' }, + expect.objectContaining({ + actorScope: { userId: 'operator-1', tenantId: 'tenant-a' }, + channelId: 'cli', + correlationId: 'corr-1', + }), + ); + }); + + it('requires a correlation header before invoking the coordination service', async () => { + const coordination = { handoff: vi.fn(), observe: vi.fn(), result: vi.fn() }; + const controller = new MosCoordinationController(coordination as never); + + await expect( + controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, undefined), + ).rejects.toThrow('X-Correlation-Id is required'); + expect(coordination.handoff).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/gateway/src/coord/mos-coordination.controller.ts b/apps/gateway/src/coord/mos-coordination.controller.ts new file mode 100644 index 0000000..cacfe7b --- /dev/null +++ b/apps/gateway/src/coord/mos-coordination.controller.ts @@ -0,0 +1,73 @@ +import { + Body, + Controller, + ForbiddenException, + Get, + Headers, + Inject, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { AuthGuard } from '../auth/auth.guard.js'; +import { CurrentUser } from '../auth/current-user.decorator.js'; +import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; +import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; +import type { + MosCoordinationObservationDto, + MosCoordinationResponseDto, + MosCoordinationResultDto, + CreateMosHandoffDto, +} from './mos-coordination.dto.js'; +import { MosCoordinationService } from './mos-coordination.service.js'; + +/** Authenticated interaction-plane boundary for the handoff/observe/result-only Mos contract. */ +@Controller('api/coord/mos') +@UseGuards(AuthGuard) +export class MosCoordinationController { + constructor( + @Inject(MosCoordinationService) private readonly coordination: MosCoordinationService, + ) {} + + @Post('handoff') + async handoff( + @Body() request: CreateMosHandoffDto, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ): Promise { + return { receipt: await this.coordination.handoff(request, this.context(user, correlationId)) }; + } + + @Get(':handoffId/observe') + async observe( + @Param('handoffId') handoffId: string, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ): Promise { + return { + observation: await this.coordination.observe(handoffId, this.context(user, correlationId)), + }; + } + + @Get(':handoffId/result') + async result( + @Param('handoffId') handoffId: string, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ): Promise { + return { result: await this.coordination.result(handoffId, this.context(user, correlationId)) }; + } + + private context( + user: AuthenticatedUserLike, + correlationId?: string, + ): RuntimeProviderRequestContext { + const requestCorrelationId = correlationId?.trim(); + if (!requestCorrelationId) throw new ForbiddenException('X-Correlation-Id is required'); + return { + actorScope: scopeFromUser(user), + channelId: 'cli', + correlationId: requestCorrelationId, + }; + } +}