feat(m7-phase2): escapeAllowsEvent — panic-предикат escape-survivability (правило 7б)

This commit is contained in:
Дмитрий
2026-06-08 10:17:43 +03:00
parent 8a9e21c280
commit f71f1abef8
2 changed files with 38 additions and 0 deletions
+16
View File
@@ -67,6 +67,22 @@ export function escapeGrantOpen(action, grants, consumed, now = Date.now()) {
return findOpenGrant(action, grants, consumed, now) !== null;
}
/**
* Panic-предикат (M7 Фаза 2, правило 7б): покрывает ли открытый escape-грант
* действие из события инструмента. Тотален (canonicalAction из Фазы 0 не бросает;
* внешний try на любой остаток → false). Для panic-веток floor/supreme-gate:
* escape оценивается даже когда per-tool-логика бросает (иначе баг = кирпич мимо escape).
*/
export function escapeAllowsEvent(event, grants, consumed, now = Date.now()) {
try {
const name = event && event.tool_name;
const input = (event && event.tool_input) || {};
return escapeGrantOpen(canonicalAction(name, input), grants, consumed, now);
} catch {
return false;
}
}
/** I/O: floor_escape-пропуски сессии (зеркало shell-content::loadApprovedGitOps). */
export function loadFloorEscapes(sessionId, now = Date.now()) {
return loadRecords(sessionId, 'floor_escape', 'action').filter((g) => now - g.ts <= FLOOR_ESCAPE_WINDOW_MS);
+22
View File
@@ -97,3 +97,25 @@ describe('canonicalAction тотальна (M7 Фаза 0, правило 7а, S
expect(canonicalAction('PowerShell', { command: 'Get-ChildItem' })).toBe('powershell:Get-ChildItem');
});
});
import { escapeAllowsEvent } from './escape-grant.mjs';
describe('escapeAllowsEvent — panic-предикат (M7 Фаза 2, правило 7б)', () => {
const now = 1_000_000;
const ev = (tool_name, tool_input) => ({ tool_name, tool_input });
it('матч-грант на действие события → true', () => {
const action = canonicalAction('Bash', { command: 'git push --force' });
const grants = [{ action, ts: now - 1000 }];
expect(escapeAllowsEvent(ev('Bash', { command: 'git push --force' }), grants, [], now)).toBe(true);
});
it('несовпавший грант → false', () => {
const grants = [{ action: 'bash:reset --hard', ts: now - 1000 }];
expect(escapeAllowsEvent(ev('Bash', { command: 'git push --force' }), grants, [], now)).toBe(false);
});
it('пустые гранты → false', () => {
expect(escapeAllowsEvent(ev('Bash', { command: 'x' }), [], [], now)).toBe(false);
});
it('малформ-event не бросает → false', () => {
expect(escapeAllowsEvent(undefined, [], [], now)).toBe(false);
expect(escapeAllowsEvent(null, null, null, now)).toBe(false);
});
});