From f71f1abef83d2e22707b682fb5f9ffdef78afa34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 8 Jun 2026 10:17:43 +0300 Subject: [PATCH] =?UTF-8?q?feat(m7-phase2):=20escapeAllowsEvent=20?= =?UTF-8?q?=E2=80=94=20panic-=D0=BF=D1=80=D0=B5=D0=B4=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=20escape-survivability=20(=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D0=BE=207=D0=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/escape-grant.mjs | 16 ++++++++++++++++ tools/escape-grant.test.mjs | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/tools/escape-grant.mjs b/tools/escape-grant.mjs index 47c20681..114c48cc 100644 --- a/tools/escape-grant.mjs +++ b/tools/escape-grant.mjs @@ -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); diff --git a/tools/escape-grant.test.mjs b/tools/escape-grant.test.mjs index 8a0b0009..10aeaf83 100644 --- a/tools/escape-grant.test.mjs +++ b/tools/escape-grant.test.mjs @@ -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); + }); +});