b6d06ede87
Блок 1 Машины 5: вето-до-плана на необратимое, независимо от членства в плане. - tools/floor-decide.mjs — чистое ядро: Bash floor (classifyDestructive whole-string + посегментно tokenizeBash — кавычки/chaining нейтрализованы) + tool-agnostic запись (P10-a: .env/ключ/cert + ~/.claude/runtime, fail-CLOSED на normalize-throw). - Дверь владельца Δ1 — read-only approve_git_operation (exact+5мин окно, НЕ consume). - tools/enforce-floor.mjs — обёртка matcher '*' (регистрация — шаг владельца, ОТДЕЛЬНО от стены М2), loadApprovedGitOps read-only, fail-CLOSED, НЕ импортирует plan-lock. - C4: migrate:fresh/refresh/reset убраны из router-gate whitelist → default-deny даже без floor-хука (SPOF-защита); bare migrate + migrate:rollback остаются. - Δ7: enforce-supreme-gate.decide на allow-пути зовёт classifyDestructive(...).floor — разрушительный in-plan шаг НЕ продвигает указатель (стена не благословляет снос). - Атака-линза: закрыт P10-a-пробел (MCP-writer в .env floor бы пропустил). Audit-context вскрыл расхождения план↔код (задокументированы в floor-decide JSDoc): writer approval НЕ подписывает (интегрити = protected-path side-channel, не HMAC); F5-гонка мнимая (loadApprovedGitOps read-only+window, не consume); force-push доп-блок shell-content GIT_HARD (дверь для него мут — защита-в-глубину). Дверь = шов под М6. Регрессия tools-only: 2649 passed + 2 skip (+41). Residual: node-whitelist hole для записи в runtime (Пакет 4 сужает); base64-обфускация floor (~0.5%, М6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
38 lines
2.0 KiB
JavaScript
38 lines
2.0 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { decide } from './enforce-floor.mjs';
|
|
|
|
// enforce-floor — тонкая обёртка floor-decide. decide() чистая (approvedGitOps инъект).
|
|
const ev = (tool_name, tool_input) => ({ tool_name, tool_input, session_id: 's1' });
|
|
|
|
describe('enforce-floor.decide — делегирует floor-decide', () => {
|
|
it('необратимая Bash без одобрения → block', () => {
|
|
const r = decide({ event: ev('Bash', { command: 'php artisan migrate:fresh' }), approvedGitOps: [] });
|
|
expect(r.block).toBe(true);
|
|
});
|
|
it('обычная Bash → не block', () => {
|
|
const r = decide({ event: ev('Bash', { command: 'git status' }), approvedGitOps: [] });
|
|
expect(r.block).toBe(false);
|
|
});
|
|
it('Read → не block', () => {
|
|
const r = decide({ event: ev('Read', { file_path: '/home/u/.env' }), approvedGitOps: [], normalizeImpl: (s) => s });
|
|
expect(r.block).toBe(false);
|
|
});
|
|
it('дверь владельца: свежее одобрение точной команды → не block', () => {
|
|
const now = 1_000_000;
|
|
const r = decide({ event: ev('Bash', { command: 'php artisan db:wipe' }), approvedGitOps: [{ command: 'php artisan db:wipe', ts: now - 1000 }], now });
|
|
expect(r.block).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('enforce-floor — пол первее плана (не импортирует plan-lock)', () => {
|
|
it('исходник enforce-floor.mjs не ИМПОРТИРУЕТ plan-lock (Δ9: floor до плана)', () => {
|
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
const src = readFileSync(join(dir, 'enforce-floor.mjs'), 'utf8');
|
|
// таргетим именно import-стейтмент, не упоминание в комментарии
|
|
expect(/(?:import|require)\b[^\n]*['"][^'"]*plan-lock/.test(src)).toBe(false);
|
|
});
|
|
});
|