feat(door-coverage): door matcher reader and canonical mutating tools list
This commit is contained in:
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user