fix: coord review remediations — path traversal, JSON parse, race condition

Addresses code review findings from P2-005:
- Validate projectPath against allowed workspace roots (path traversal)
- Guard JSON.parse with try/catch in loadMission, readActiveSession, readSessionLock
- Add delay after stale lock removal to reduce race window
- Add @Inject(CoordService) per project guideline (no emitDecoratorMetadata)
- Eliminate double loadMission in getTaskStatus via shared buildStatusSummary
- Fix fragile prompt-inclusion check to test original command for {prompt}
- Add mkdir to writeAtomic for consistency with other atomic helpers

Closes #80

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 22:43:30 -05:00
parent b03c603759
commit 4de23e238a
5 changed files with 86 additions and 34 deletions

View File

@@ -1,15 +1,39 @@
import { Controller, Get, NotFoundException, Param, Query, UseGuards } from '@nestjs/common';
import {
BadRequestException,
Controller,
Get,
Inject,
NotFoundException,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import path from 'node:path';
import { AuthGuard } from '../auth/auth.guard.js';
import { CoordService } from './coord.service.js';
/** Only paths under these roots are allowed for coord queries. */
const ALLOWED_ROOTS = [process.cwd()];
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;
}
@Controller('api/coord')
@UseGuards(AuthGuard)
export class CoordController {
constructor(private readonly coordService: CoordService) {}
constructor(@Inject(CoordService) private readonly coordService: CoordService) {}
@Get('status')
async missionStatus(@Query('projectPath') projectPath?: string) {
const resolvedPath = projectPath ?? process.cwd();
const resolvedPath = resolveAndValidatePath(projectPath);
const status = await this.coordService.getMissionStatus(resolvedPath);
if (!status) throw new NotFoundException('No active coord mission found');
return status;
@@ -17,13 +41,13 @@ export class CoordController {
@Get('tasks')
async listTasks(@Query('projectPath') projectPath?: string) {
const resolvedPath = projectPath ?? process.cwd();
const resolvedPath = resolveAndValidatePath(projectPath);
return this.coordService.listTasks(resolvedPath);
}
@Get('tasks/:taskId')
async taskStatus(@Param('taskId') taskId: string, @Query('projectPath') projectPath?: string) {
const resolvedPath = projectPath ?? process.cwd();
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;