feat(door-coverage): door matcher reader and canonical mutating tools list

This commit is contained in:
Дмитрий
2026-06-09 04:02:27 +03:00
parent 2e6a6c0a7a
commit 0eef670e54
4 changed files with 90 additions and 1 deletions
+28
View File
@@ -21,3 +21,31 @@ 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 [];
}
+46
View File
@@ -37,3 +37,49 @@ describe('auditExempt (страховка зелёного прохода, F)',
expect(r.flagged).toEqual([]);
});
});
// ── R-24 (Блок B Класс 2): door-coverage helpers + anti-drift seeds ──
import { CANONICAL_MUTATING_TOOLS, isMutatingTool, extractGateMatcher } from './door-coverage.mjs';
import { SEED_TOOLS as GATE_SEED_TOOLS } from './enforce-supreme-gate.mjs';
describe('CANONICAL_MUTATING_TOOLS / isMutatingTool (R-24)', () => {
it('канонический список включает ключевые мутирующие инструменты', () => {
for (const t of ['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash', 'Task', 'Skill']) {
expect(CANONICAL_MUTATING_TOOLS).toContain(t);
}
});
it('isMutatingTool: мутирующий → true, observe-only → false', () => {
expect(isMutatingTool('Write')).toBe(true);
expect(isMutatingTool('Read')).toBe(false);
expect(isMutatingTool('EnterPlanMode')).toBe(false);
});
});
describe('extractGateMatcher (R-24)', () => {
it('хук не найден → []', () => {
expect(extractGateMatcher({}, 'enforce-supreme-gate.mjs')).toEqual([]);
expect(extractGateMatcher({ hooks: { PreToolUse: [] } }, 'enforce-supreme-gate.mjs')).toEqual([]);
});
it('matcher "*" → ["*"]', () => {
const settings = { hooks: { PreToolUse: [{ matcher: '*', hooks: [{ type: 'command', command: 'node tools/enforce-supreme-gate.mjs' }] }] } };
expect(extractGateMatcher(settings, 'enforce-supreme-gate.mjs')).toEqual(['*']);
});
it('matcher "Edit|Write|Bash" → ["Edit","Write","Bash"]', () => {
const settings = { hooks: { PreToolUse: [{ matcher: 'Edit|Write|Bash', hooks: [{ command: 'node tools/enforce-supreme-gate.mjs' }] }] } };
expect(extractGateMatcher(settings, 'enforce-supreme-gate.mjs')).toEqual(['Edit', 'Write', 'Bash']);
});
it('пустой matcher "" → [] (аудит требует явного *)', () => {
const settings = { hooks: { PreToolUse: [{ matcher: '', hooks: [{ command: 'node tools/enforce-supreme-gate.mjs' }] }] } };
expect(extractGateMatcher(settings, 'enforce-supreme-gate.mjs')).toEqual([]);
});
});
describe('SEED_TOOLS export (R-24 anti-drift, инвариантность)', () => {
it('экспортирован и содержит seed-инструменты стены', () => {
expect(GATE_SEED_TOOLS.has('EnterPlanMode')).toBe(true);
expect(GATE_SEED_TOOLS.has('AskUserQuestion')).toBe(true);
});
it('seed-инструменты НЕ числятся мутирующими (иначе auditExempt их флагует)', () => {
for (const t of GATE_SEED_TOOLS) expect(isMutatingTool(t)).toBe(false);
});
});
+1 -1
View File
@@ -22,7 +22,7 @@ import { canonicalAction, escapeGrantOpen, escapeAllowsEvent, loadFloorEscapes,
// нельзя вызвать). Это Skill-вызовы (не мутируют мир) → seed-allow безопасен.
export const SEED_SKILLS = ['writing-plans', 'brainstorming', 'discovery-interview',
'systematic-debugging', 'test-driven-development', 'requesting-code-review', 'verification-before-completion'];
const SEED_TOOLS = new Set(['EnterPlanMode', 'AskUserQuestion']);
export const SEED_TOOLS = new Set(['EnterPlanMode', 'AskUserQuestion']);
function skillSuffix(name) { const s = String(name || '').toLowerCase(); return s.includes(':') ? s.split(':').pop() : s; }
+15
View File
@@ -441,3 +441,18 @@ describe('runGate — FIX-3: escape best-effort журнал без продви
expect(r.block).toBe(false);
});
});
// R-24 (Блок B Класс 2): SEED_TOOLS экспортируется для read-only аудита покрытия дверей.
// Аддитивный export — гейт-решения неизменны; пиннинг состава seed-инструментов (анти-дрейф).
import { SEED_TOOLS } from './enforce-supreme-gate.mjs';
describe('R-24: SEED_TOOLS export (инвариантность состава)', () => {
it('экспортирован как Set с EnterPlanMode + AskUserQuestion', () => {
expect(SEED_TOOLS instanceof Set).toBe(true);
expect(SEED_TOOLS.has('EnterPlanMode')).toBe(true);
expect(SEED_TOOLS.has('AskUserQuestion')).toBe(true);
});
it('seed-инструменты не считаются seed-навыками (тулы, не Skill)', () => {
expect(isSeed({ name: 'EnterPlanMode' })).toBe(true);
expect(isSeed({ name: 'AskUserQuestion' })).toBe(true);
});
});