Files
portal/tools/judge-subrun-journal.mjs
T
Дмитрий e1a6f26c06 fix(router-mentor): close sessionId path-traversal class across M1-M4
Аудит M1-M4 (audit-context-building) нашёл непоследовательность guard формы
sessionId: N3-фикс защитил только action-journal.paths() (M1), а 4 sibling-
строителя пути из event.session_id (недоверенный источник) остались без проверки.

Единый экспорт assertSafeSessionId (action-journal.mjs, переиспользует SESSION_ID_RE
N3) применён во всех точках машин:
- M1 action-journal.paths() — рефактор на общий guard (поведение N3 сохранено)
- M4 judge-subrun-journal.paths() — guard добавлен (канал прилежности судьи F1)
- M2 plan-lock.planPath + artifactPath — guard добавлен
- M2 enforce-supreme-gate — экспортируемый guarded stepStatePath, применён в main()

TDD RED-GREEN на каждом файле. Регрессия tools-only 2540 passed + 2 skip (+10).
Серьёзность класса — низкая / защита-в-глубину (sessionId harness-controlled),
закрыт ради консистентности (был 1 из 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 05:41:07 +03:00

63 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/**
* judge-subrun-journal (F1, §9.1) — НЕподделываемый контроллером канал прилежности
* судьи. Под-прогон (реальный запуск думающего навыка судьёй) пишется в ОТДЕЛЬНЫЙ
* judge-only файл judge-subrun-<sess>.jsonl, цепочка+голова подписаны КЛЮЧОМ СУДЬИ
* (router-mentor-judge). Контроллер пишет только в общий action-journal — туда
* запись «судья прогнал X» доверять НЕЛЬЗЯ; доверяем лишь этому каналу под judge-ключом.
* Переиспользует хеш-цепочку Машины 1 (appendEntry/verifyChain), отдельный путь+ключ.
*/
import fsDefault from 'node:fs';
import { appendEntry, verifyChain, assertSafeSessionId } from './action-journal.mjs';
function paths(runtimeDir, sessionId) {
assertSafeSessionId(sessionId); // N3-shared: тот же guard формы, что в action-journal
const sep = runtimeDir.endsWith('/') ? '' : '/';
return {
jsonl: `${runtimeDir}${sep}judge-subrun-${sessionId}.jsonl`,
head: `${runtimeDir}${sep}judge-subrun-${sessionId}.head`,
};
}
/** Прочитать judge-only журнал под-прогонов (+ подпись головы). Нет файла → пусто. */
export function loadJudgeSubRuns({ sessionId, runtimeDir, fsImpl = fsDefault }) {
const { jsonl, head } = paths(runtimeDir, sessionId);
let entries = [];
try {
const raw = fsImpl.readFileSync(jsonl, 'utf8');
entries = String(raw).split('\n').filter(Boolean).map((l) => JSON.parse(l));
} catch (e) { if (e && e.code === 'ENOENT') return { entries: [], headSig: null }; throw e; }
let headSig = null;
try { headSig = String(fsImpl.readFileSync(head, 'utf8')).trim() || null; } catch { headSig = null; }
return { entries, headSig };
}
/** Добавить под-прогон судьи (персист через Node fs, подпись головы judge-ключом). */
export function appendJudgeSubRun({ payload, key, sessionId, runtimeDir, nowMs, fsImpl = fsDefault }) {
const { entries } = loadJudgeSubRuns({ sessionId, runtimeDir, fsImpl });
const { entries: next, headSig } = appendEntry(entries, payload, { key, nowMs });
const entry = next[next.length - 1];
const { jsonl, head } = paths(runtimeDir, sessionId);
fsImpl.appendFileSync(jsonl, JSON.stringify(entry) + '\n');
fsImpl.writeFileSync(head, String(headSig ?? ''));
return { entry, headSig };
}
/** Цепочка цела И голова подписана judge-ключом? (контроллер без ключа → false). */
export function verifyJudgeJournal(entries, headSig, judgeKey) {
return verifyChain(entries, headSig, { key: judgeKey });
}
/**
* Дисциплина #1: для каждой требующей реального запуска линзы вердикта в журнале
* должен быть совпавший под-прогон. required: [{lens, skill?}]; subRuns: payload'ы.
* Совпадение по lens (+ skill, если задан в required). Нет → missing (вердикт фейк).
*/
export function requiredSubRunsPresent(required = [], subRuns = []) {
const missing = (required || []).filter((req) =>
!(subRuns || []).some((r) =>
r.lens === req.lens && (req.skill === undefined || r.skill === req.skill))
);
return { ok: missing.length === 0, missing };
}