Files
brain/tools/enforce-read-path-deny.mjs
T

73 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();