b6d06ede87
Блок 1 Машины 5: вето-до-плана на необратимое, независимо от членства в плане. - tools/floor-decide.mjs — чистое ядро: Bash floor (classifyDestructive whole-string + посегментно tokenizeBash — кавычки/chaining нейтрализованы) + tool-agnostic запись (P10-a: .env/ключ/cert + ~/.claude/runtime, fail-CLOSED на normalize-throw). - Дверь владельца Δ1 — read-only approve_git_operation (exact+5мин окно, НЕ consume). - tools/enforce-floor.mjs — обёртка matcher '*' (регистрация — шаг владельца, ОТДЕЛЬНО от стены М2), loadApprovedGitOps read-only, fail-CLOSED, НЕ импортирует plan-lock. - C4: migrate:fresh/refresh/reset убраны из router-gate whitelist → default-deny даже без floor-хука (SPOF-защита); bare migrate + migrate:rollback остаются. - Δ7: enforce-supreme-gate.decide на allow-пути зовёт classifyDestructive(...).floor — разрушительный in-plan шаг НЕ продвигает указатель (стена не благословляет снос). - Атака-линза: закрыт P10-a-пробел (MCP-writer в .env floor бы пропустил). Audit-context вскрыл расхождения план↔код (задокументированы в floor-decide JSDoc): writer approval НЕ подписывает (интегрити = protected-path side-channel, не HMAC); F5-гонка мнимая (loadApprovedGitOps read-only+window, не consume); force-push доп-блок shell-content GIT_HARD (дверь для него мут — защита-в-глубину). Дверь = шов под М6. Регрессия tools-only: 2649 passed + 2 skip (+41). Residual: node-whitelist hole для записи в runtime (Пакет 4 сужает); base64-обфускация floor (~0.5%, М6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
40 lines
2.1 KiB
JavaScript
40 lines
2.1 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* enforce-floor (Машина 5, Блок 1) — обёртка несущего пола. Matcher '*'
|
||
* (регистрация в settings.json — шаг ВЛАДЕЛЬЦА, ОТДЕЛЬНО от верховной стены М2:
|
||
* снятие стены не снимает пол; чтобы пробить необратимое — нужно снять оба).
|
||
*
|
||
* Зовёт floor-decide ПЕРВЫМ (до seed/observe/членства в плане). Дверь владельца —
|
||
* read-only approve_git_operation через shell-content::loadApprovedGitOps (тот же
|
||
* window+read-only механизм, что у router-gate; floor НЕ потребляет — F5 гонки нет).
|
||
*
|
||
* НЕ импортирует plan-lock (Δ9: пол первее плана). fail-CLOSED: любая ошибка → block.
|
||
*/
|
||
import { readStdin, parseEventJson, exitDecision } from './enforce-hook-helpers.mjs';
|
||
import { floorDecide } from './floor-decide.mjs';
|
||
import { loadApprovedGitOps } from './shell-content-rules.mjs';
|
||
|
||
/** Чистое решение: делегирует floor-decide. approvedGitOps/now/normalizeImpl инъектируемы. */
|
||
export function decide({ event, approvedGitOps = [], now = Date.now(), normalizeImpl }) {
|
||
const toolUse = { name: event && event.tool_name, input: (event && event.tool_input) || {} };
|
||
const args = { toolUse, approvedGitOps, now };
|
||
if (normalizeImpl) args.normalizeImpl = normalizeImpl;
|
||
return floorDecide(args);
|
||
}
|
||
|
||
async function main() {
|
||
try {
|
||
const event = parseEventJson(await readStdin());
|
||
const sess = (event && event.session_id) || 'unknown';
|
||
const approvedGitOps = loadApprovedGitOps(sess); // read-only, window-filtered
|
||
const r = decide({ event, approvedGitOps });
|
||
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();
|