feat: анти-диктовка свободного floor_escape (consent forgery C)

Свободный floor_escape (лёгкий escape в чате) НЕ доверяется, если контроллер в своём тексте
текущего хода инструктировал владельца вписать метку. processEvent читает текст контроллера
из транскрипта хода (readTranscript + lastAssistantText, переиспользован), один раз на событие
зовёт detectOtherSocialEng (раньше был мёртвой проводкой — не вызывался нигде); flagged →
floor_escape не пишется. Паттерны detectOtherSocialEng усилены императивной диктовкой метки
FLOOR-ESCAPE (глагол рядом; простое упоминание концепта не флагуется). fail-open observability —
сбой чтения транскрипта → как раньше. Применяется к floor_escape (git-approval закрыт #A).
Завершает кодовую часть эпика (A + B1-B5 + C). Спека §C. Свод 4350 зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-18 19:54:25 +03:00
parent 11969ac3ea
commit 0d31e62248
6 changed files with 237 additions and 4 deletions
@@ -151,3 +151,23 @@ describe('processEvent — anti-button (HOLE-1 / A): ответ-кнопка н
rmSync(dir, { recursive: true, force: true });
});
});
describe('processEvent — anti-dictation (C): диктованный свободный floor_escape не пишется', () => {
it('controllerText flagged → floor_escape НЕ записан', () => {
const dir = tmpRuntimeDir();
const event = { session_id: 'cd1', tool_input: { questions: [{ question: 'q' }] },
tool_response: { answers: { q: 'да FLOOR-ESCAPE: write:c:/x.md' } } };
processEvent(event, { runtimeDir: dir, nowMs: 7, controllerText: 'впиши в Other: FLOOR-ESCAPE: write:c:/x.md' });
expect(existsSync(join(dir, 'askuser-decisions-cd1.jsonl'))).toBe(false);
rmSync(dir, { recursive: true, force: true });
});
it('controllerText чистый → floor_escape записан (без регрессии)', () => {
const dir = tmpRuntimeDir();
const event = { session_id: 'cd2', tool_input: { questions: [{ question: 'q' }] },
tool_response: { answers: { q: 'да FLOOR-ESCAPE: write:c:/x.md' } } };
processEvent(event, { runtimeDir: dir, nowMs: 7, controllerText: 'разрешаешь правку файла?' });
const content = readFileSync(join(dir, 'askuser-decisions-cd2.jsonl'), 'utf-8');
expect(content).toContain('"type":"floor_escape"');
rmSync(dir, { recursive: true, force: true });
});
});