import { BadRequestException, Controller, Get, Inject, NotFoundException, Param, Query, UseGuards, } from '@nestjs/common'; import fs from 'node:fs'; import path from 'node:path'; import { AuthGuard } from '../auth/auth.guard.js'; import { CoordService } from './coord.service.js'; /** Walk up from cwd to find the monorepo root (has pnpm-workspace.yaml). */ function findMonorepoRoot(start: string): string { let dir = start; for (let i = 0; i < 5; i++) { try { fs.accessSync(path.join(dir, 'pnpm-workspace.yaml')); return dir; } catch { const parent = path.dirname(dir); if (parent === dir) break; dir = parent; } } return start; } /** Only paths under these roots are allowed for coord queries. */ const WORKSPACE_ROOT = process.env['MOSAIC_WORKSPACE_ROOT'] ?? findMonorepoRoot(process.cwd()); const ALLOWED_ROOTS = [process.cwd(), WORKSPACE_ROOT]; function resolveAndValidatePath(raw: string | undefined): string { const resolved = path.resolve(raw ?? process.cwd()); const isAllowed = ALLOWED_ROOTS.some( (root) => resolved === root || resolved.startsWith(`${root}/`), ); if (!isAllowed) { throw new BadRequestException('projectPath is outside the allowed workspace'); } return resolved; } /** * File-based coord endpoints for agent tool consumption. * DB-backed mission CRUD has moved to MissionsController at /api/missions. */ @Controller('api/coord') @UseGuards(AuthGuard) export class CoordController { constructor(@Inject(CoordService) private readonly coordService: CoordService) {} @Get('status') async missionStatus(@Query('projectPath') projectPath?: string) { const resolvedPath = resolveAndValidatePath(projectPath); const status = await this.coordService.getMissionStatus(resolvedPath); if (!status) throw new NotFoundException('No active coord mission found'); return status; } @Get('tasks') async listTasks(@Query('projectPath') projectPath?: string) { const resolvedPath = resolveAndValidatePath(projectPath); return this.coordService.listTasks(resolvedPath); } @Get('tasks/:taskId') async taskStatus(@Param('taskId') taskId: string, @Query('projectPath') projectPath?: string) { const resolvedPath = resolveAndValidatePath(projectPath); const detail = await this.coordService.getTaskStatus(resolvedPath, taskId); if (!detail) throw new NotFoundException(`Task ${taskId} not found in coord mission`); return detail; } }