397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
52 lines
2.7 KiB
JavaScript
52 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* door-coverage — авто-аудит покрытия дверей (P15-b): инструмент, не покрытый
|
|
* matcher'ом верховного хука и не семя, = «забытая дверь» → флаг. Закрывает
|
|
* пропущенный канал как КЛАСС, а не точечно (урок F1: PowerShell мимо Bash).
|
|
*/
|
|
export function auditDoors({ tools = [], matcher = [], seeds = [] }) {
|
|
const star = matcher.includes('*');
|
|
const cover = new Set(matcher);
|
|
const seed = new Set(seeds);
|
|
const uncovered = star ? [] : tools.filter((t) => !cover.has(t) && !seed.has(t));
|
|
return { ok: uncovered.length === 0, uncovered };
|
|
}
|
|
|
|
/**
|
|
* Страховка к D (F): инструмент в «зелёном проходе» (seeds/observe), чья способность
|
|
* мутирующая, = опасное исключение → флаг. isMutating инъектируется (общий критерий
|
|
* способности с supreme-gate), чтобы не плодить зашитых списков.
|
|
*/
|
|
export function auditExempt({ exempt = [], isMutating }) {
|
|
const flagged = exempt.filter((t) => isMutating(t));
|
|
return { ok: flagged.length === 0, flagged };
|
|
}
|
|
|
|
/** Канонический набор мутирующих инструментов (по способности, выровнен с supreme-gate
|
|
* default-deny: всё, что не observe-only и не seed). MCP-писатели динамические — не статичны. */
|
|
export const CANONICAL_MUTATING_TOOLS = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash', 'Task', 'Skill'];
|
|
|
|
export function isMutatingTool(name) {
|
|
return CANONICAL_MUTATING_TOOLS.includes(name);
|
|
}
|
|
|
|
/**
|
|
* Извлечь, какие инструменты покрывает matcher хука <basename> в settings.PreToolUse.
|
|
* '*' → ['*'] (покрывает всё); "A|B" → ['A','B']; хук не найден / пустой matcher → []
|
|
* (пустой = аудит требует явного '*', иначе сигналим «дверь не покрыта»).
|
|
*/
|
|
export function extractGateMatcher(settings, hookBasename) {
|
|
const pre = settings && settings.hooks && settings.hooks.PreToolUse;
|
|
if (!Array.isArray(pre)) return [];
|
|
for (const entry of pre) {
|
|
const inner = entry && entry.hooks;
|
|
if (!Array.isArray(inner)) continue;
|
|
const has = inner.some((h) => h && typeof h.command === 'string' && h.command.includes(hookBasename));
|
|
if (!has) continue;
|
|
const m = typeof entry.matcher === 'string' ? entry.matcher : '';
|
|
if (m === '*') return ['*'];
|
|
return m.split('|').map((s) => s.trim()).filter(Boolean);
|
|
}
|
|
return [];
|
|
}
|