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

@@ -195,11 +195,12 @@ function resolveLaunchCommand(
return [runtime, '-p', prompt];
}
const hasPromptPlaceholder = configuredCommand.some((value) => value === '{prompt}');
const withInterpolation = configuredCommand.map((value) =>
value === '{prompt}' ? prompt : value,
);
if (withInterpolation.includes(prompt)) {
if (hasPromptPlaceholder) {
return withInterpolation;
}
@@ -224,28 +225,9 @@ async function writeAtomicJson(filePath: string, payload: unknown): Promise<void
async function readSessionLock(mission: Mission): Promise<SessionLockState | undefined> {
const filePath = sessionLockPath(mission);
let raw: string;
try {
const raw = await fs.readFile(filePath, 'utf8');
const data = JSON.parse(raw) as Partial<SessionLockState>;
if (
typeof data.session_id !== 'string' ||
(data.runtime !== 'claude' && data.runtime !== 'codex') ||
typeof data.pid !== 'number' ||
typeof data.started_at !== 'string' ||
typeof data.project_path !== 'string'
) {
return undefined;
}
return {
session_id: data.session_id,
runtime: data.runtime,
pid: data.pid,
started_at: data.started_at,
project_path: data.project_path,
milestone_id: data.milestone_id,
};
raw = await fs.readFile(filePath, 'utf8');
} catch (error) {
if (
typeof error === 'object' &&
@@ -257,6 +239,32 @@ async function readSessionLock(mission: Mission): Promise<SessionLockState | und
}
throw error;
}
let data: Partial<SessionLockState>;
try {
data = JSON.parse(raw) as Partial<SessionLockState>;
} catch {
return undefined;
}
if (
typeof data.session_id !== 'string' ||
(data.runtime !== 'claude' && data.runtime !== 'codex') ||
typeof data.pid !== 'number' ||
typeof data.started_at !== 'string' ||
typeof data.project_path !== 'string'
) {
return undefined;
}
return {
session_id: data.session_id,
runtime: data.runtime,
pid: data.pid,
started_at: data.started_at,
project_path: data.project_path,
milestone_id: data.milestone_id,
};
}
async function writeSessionLock(mission: Mission, lock: SessionLockState): Promise<void> {