2026-06-07 11:42:58 +03:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* enforce-floor (Машина 5, Блок 1) — обёртка несущего пола. Matcher '*'
|
|
|
|
|
|
* (регистрация в settings.json — шаг ВЛАДЕЛЬЦА, ОТДЕЛЬНО от верховной стены М2:
|
|
|
|
|
|
* снятие стены не снимает пол; чтобы пробить необратимое — нужно снять оба).
|
|
|
|
|
|
*
|
2026-06-07 18:52:34 +03:00
|
|
|
|
* Зовёт floor-decide ПЕРВЫМ (до seed/observe/членства в плане). Аварийный выход владельца —
|
|
|
|
|
|
* read-only floor_escape-пропуски через escape-grant::loadFloorEscapes + отметки погашения
|
|
|
|
|
|
* loadConsumed (floor НЕ потребляет — one-shot делает PostToolUse-консьюмер; гонки нет).
|
2026-06-07 11:42:58 +03:00
|
|
|
|
*
|
|
|
|
|
|
* НЕ импортирует plan-lock (Δ9: пол первее плана). fail-CLOSED: любая ошибка → block.
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { readStdin, parseEventJson, exitDecision } from './enforce-hook-helpers.mjs';
|
|
|
|
|
|
import { floorDecide } from './floor-decide.mjs';
|
2026-06-08 10:19:48 +03:00
|
|
|
|
import { loadFloorEscapes, loadConsumed, escapeAllowsEvent } from './escape-grant.mjs';
|
2026-06-10 04:28:53 +03:00
|
|
|
|
import { logGuardBlock } from './guard-block-log.mjs';
|
2026-06-07 11:42:58 +03:00
|
|
|
|
|
2026-06-08 10:19:48 +03:00
|
|
|
|
/** Чистое решение: делегирует floor-decide. escapeGrants/escapeConsumed/now/normalizeImpl инъектируемы.
|
|
|
|
|
|
* M7 Фаза 2 (правило 7б): floorDecide обёрнут в try — если он бросит ДО своего escape-чека,
|
|
|
|
|
|
* panic-ветка всё равно оценивает escape владельца (иначе баг = кирпич мимо escape).
|
|
|
|
|
|
* floorDecideImpl инъектируем для теста panic-пути. */
|
|
|
|
|
|
export function decide({ event, escapeGrants = [], escapeConsumed = [], now = Date.now(), normalizeImpl, floorDecideImpl = floorDecide }) {
|
2026-06-07 11:42:58 +03:00
|
|
|
|
const toolUse = { name: event && event.tool_name, input: (event && event.tool_input) || {} };
|
2026-06-07 18:52:34 +03:00
|
|
|
|
const args = { toolUse, escapeGrants, escapeConsumed, now };
|
2026-06-07 11:42:58 +03:00
|
|
|
|
if (normalizeImpl) args.normalizeImpl = normalizeImpl;
|
2026-06-08 10:19:48 +03:00
|
|
|
|
try {
|
|
|
|
|
|
return floorDecideImpl(args);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
if (escapeAllowsEvent(event, escapeGrants, escapeConsumed, now)) {
|
|
|
|
|
|
return { block: false, reason: 'floor: panic-escape (floorDecide бросил, escape владельца чтится)' };
|
|
|
|
|
|
}
|
|
|
|
|
|
return { block: true, reason: 'floor: внутренняя ошибка вычисления — fail-CLOSED' };
|
|
|
|
|
|
}
|
2026-06-07 11:42:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const event = parseEventJson(await readStdin());
|
|
|
|
|
|
const sess = (event && event.session_id) || 'unknown';
|
2026-06-07 18:52:34 +03:00
|
|
|
|
const escapeGrants = loadFloorEscapes(sess); // read-only, window-filtered
|
|
|
|
|
|
const escapeConsumed = loadConsumed(sess); // отметки one-shot погашения
|
|
|
|
|
|
const r = decide({ event, escapeGrants, escapeConsumed });
|
2026-06-10 04:28:53 +03:00
|
|
|
|
if (r.block) logGuardBlock(event, 'М5 Пол', r.reason);
|
2026-06-07 11:42:58 +03:00
|
|
|
|
exitDecision({ block: r.block, message: r.block ? `[floor] ${r.reason}` : undefined });
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
exitDecision({ block: true, message: '[floor] внутренняя ошибка — fail-CLOSED' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
|
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
|
|
|
|
if (isCli) main();
|