84231a1470
Доска «кто на посту» (STATUS.md §7) теперь показывает реальные недавние escape владельца и блоки машин М1–М6 вместо хардкода []/[]. - new tools/guard-block-log.mjs: logGuardBlock (best-effort, fail-quiet, Node fs append в guard-blocks-<sess>.jsonl) + loadRecentBlocks/ loadRecentEscapes (скан session-файлов runtime, окно 24ч + cap 10, ts→ISO). - проводка logGuardBlock в block-ветку main() 9 машинных хуков (floor / supreme-gate / judge-gate / snapshot / read-path-deny / mcp-classification / normative-content-rules / verify-gate / criterion-gate). Логгер вызывается ПОСЛЕ решения, не влияет на block; decide() pure не тронут. - status-md-generator CLI: recentEscapes/recentBlocks из читателей вместо []/[]. До флипа Фазы 8 доска показывает 0/0 (хуки не зарегистрированы — данных нет); реальная польза — пост-флип наблюдаемость. TDD: guard-block-log.test (6) + 9 структурных wiring-тестов + 1 board-тест. Гейт закрытия: sharp-edges (промежуточный по 9 хукам + читатели) + variant-analysis (все block-ветки покрыты, иных источников нет). Регрессия tools-only 3465 passed / 2 skipped / 0 failed (было 3449+2skip). 0 регрессий. Plan: docs/superpowers/plans/2026-06-10-guard-board-live-source.md
73 lines
3.6 KiB
JavaScript
73 lines
3.6 KiB
JavaScript
/**
|
||
* PreToolUse(Read) wrapper — path-deny for Read tool.
|
||
* Router-gate v4 emergency fix (Smoke 5 2026-05-30).
|
||
*
|
||
* Spec §3.1 declared transcript JSONL hard-deny but Read tool had NO
|
||
* path-protection — controller could Read ~/.claude/projects/*.jsonl
|
||
* (parent context exfil from other sessions). Same for runtime artifacts,
|
||
* .env, normative files.
|
||
*
|
||
* Reuses DEFAULT_PROTECTED_PATTERNS from shell-content-rules.mjs.
|
||
* Fail-CLOSE on internal error (security default).
|
||
*/
|
||
import { fileURLToPath } from 'url';
|
||
import {
|
||
readStdin,
|
||
parseEventJson,
|
||
exitDecision,
|
||
} from './enforce-hook-helpers.mjs';
|
||
import { defaultPathNormalize, isProtectedPath, READ_DENY_PATTERNS } from './shell-content-rules.mjs';
|
||
import { canonicalAction, escapeGrantOpen, loadFloorEscapes, loadConsumed } from './escape-grant.mjs';
|
||
import { logGuardBlock } from './guard-block-log.mjs';
|
||
|
||
export function decide({ toolName, filePath, escapeGrants = [], escapeConsumed = [], now = Date.now() }) {
|
||
if (toolName !== 'Read') return { block: false, reason: null };
|
||
const fp = String(filePath || '');
|
||
// Path-deny only. Narrow READ_DENY_PATTERNS (not the full DEFAULT_PROTECTED_PATTERNS):
|
||
// Read of CLAUDE.md / normative docs / memory has no exfil value and must stay allowed for
|
||
// the claude-md-management / memory-sync workflow. Only genuine Read-exfil targets —
|
||
// transcripts, runtime, settings, secrets — are blocked. The full protected-list still
|
||
// guards Bash/PowerShell read and Write (over-block fix 2026-05-31).
|
||
// F-1 (аудит 2026-06-07): контент-скан выдачи убран — был МЁРТВ в проде (PreToolUse(Read)-хук,
|
||
// main() не передавал content; контента до чтения нет). Реальный exfil (исходящий payload)
|
||
// закрыт живым enforce-mcp-classification.scanEgress. decide() гейтит строго по пути.
|
||
if (fp && isProtectedPath(fp, defaultPathNormalize, READ_DENY_PATTERNS)) {
|
||
// M7 Фаза 2 (правило 7в): escape владельца снимает Read-блок защищённого пути
|
||
// (страж обязан чтить аварийный выход, как пол/стена/egress). Канон-строка действия —
|
||
// тот же binding-ключ, что владелец вставляет в AskUser-escape.
|
||
const action = canonicalAction('Read', { file_path: fp });
|
||
if (escapeGrantOpen(action, escapeGrants, escapeConsumed, now)) {
|
||
return { block: false, reason: null };
|
||
}
|
||
return {
|
||
block: true,
|
||
reason: `path «${defaultPathNormalize(fp)}» protected against Read (§3.1 transcript/runtime/secrets hard-deny); FLOOR-ESCAPE: ${action}`,
|
||
};
|
||
}
|
||
return { block: false, reason: null };
|
||
}
|
||
|
||
async function main() {
|
||
try {
|
||
const raw = await readStdin();
|
||
const event = parseEventJson(raw);
|
||
const sess = (event && event.session_id) || 'unknown';
|
||
const r = decide({
|
||
toolName: event.tool_name,
|
||
filePath: event.tool_input?.file_path || event.tool_input?.filePath,
|
||
escapeGrants: loadFloorEscapes(sess),
|
||
escapeConsumed: loadConsumed(sess),
|
||
});
|
||
if (r.block) {
|
||
logGuardBlock(event, 'М5 Read-страж', r.reason);
|
||
return exitDecision({ block: true, message: `[read-path-deny] ${r.reason}` });
|
||
}
|
||
return exitDecision({ block: false });
|
||
} catch {
|
||
return exitDecision({ block: true, message: '[read-path-deny] внутренняя ошибка — fail-CLOSE' });
|
||
}
|
||
}
|
||
|
||
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
||
if (isCli) main();
|