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>
41 lines
2.0 KiB
JavaScript
41 lines
2.0 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
||
import { decide } from './enforce-supreme-gate.mjs';
|
||
import { classifyDestructive } from './classify-destructive.mjs';
|
||
|
||
// Δ7 (Машина 5 Пакет 2.5): defense-in-depth М2. Даже если разрушительное действие
|
||
// СОВПАДАЕТ с шагом замороженного плана, стена НЕ продвигает указатель — пол
|
||
// (classify-destructive.mjs) требует двери владельца. При незарегистрированном
|
||
// floor-хуке стена всё равно не благословляет снос.
|
||
|
||
const key = 'k';
|
||
const args = (object) => ({
|
||
toolUse: { name: 'Bash', input: { command: object } },
|
||
frozenPlan: { plan_id: 'p1', steps: [{ n: 1, op: 'Bash', object }] },
|
||
frozenArtifact: null,
|
||
stepPtr: 0,
|
||
key,
|
||
verifyImpl: () => true,
|
||
verifyArtifactImpl: () => true,
|
||
normalize: (s) => s,
|
||
});
|
||
|
||
describe('decide — Δ7: разрушительное in-plan не благословляется', () => {
|
||
const DESTRUCTIVE = ['rm -rf build', 'php artisan migrate:fresh', 'git push --force', 'git reset --hard HEAD~1'];
|
||
for (const cmd of DESTRUCTIVE) {
|
||
it(`разрушительный in-plan шаг → block, указатель не двигается: ${cmd}`, () => {
|
||
expect(classifyDestructive(cmd).floor).toBe(true); // пол считает это необратимым
|
||
const r = decide(args(cmd));
|
||
expect(r.decision).toBe('block');
|
||
expect(r.advanceTo).toBeUndefined();
|
||
});
|
||
}
|
||
|
||
it('обычный (не-floor, не-observe) in-plan шаг → allow + advanceTo', () => {
|
||
// php artisan migrate: не observe-only (не readonly) и не floor (N1) → доходит до allow-пути
|
||
expect(classifyDestructive('php artisan migrate').floor).toBe(false);
|
||
const r = decide(args('php artisan migrate'));
|
||
expect(r.decision).toBe('allow');
|
||
expect(r.advanceTo).toBe(1);
|
||
});
|
||
});
|