feat(supreme-gate): tree leaf resolution plus advance flag for R-08 waves
This commit is contained in:
@@ -6,7 +6,8 @@
|
||||
* нельзя загрузить). fail-CLOSED (сбой → стоп; рубильник у владельца).
|
||||
*/
|
||||
import { readStdin, parseEventJson, exitDecision } from './enforce-hook-helpers.mjs';
|
||||
import { verifyFrozenPlan, verifyFrozenArtifact, actionMatchesStep, nextStep, refResolves } from './plan-lock.mjs';
|
||||
import { verifyFrozenPlan, verifyFrozenArtifact, actionMatchesStep, nextStep, refResolves, treeLeafAt, validatePlanTree } from './plan-lock.mjs';
|
||||
import { advanceOverTree, serializePointer, deserializePointer, normalizeToLeaf } from './step-pointer.mjs';
|
||||
import { classifyBashCommand, READING_CMDS } from './enforce-router-gate.mjs';
|
||||
import { tokenizeBash } from './bash-tokenizer.mjs';
|
||||
import { signPayload, verifyReceipt, RECEIPT_DOMAINS } from './receipt-sign.mjs';
|
||||
@@ -135,8 +136,11 @@ export function decide({ toolUse, frozenPlan, frozenArtifact = null, stepPtr = 0
|
||||
if (isObserveOnly(toolUse)) return { decision: 'allow', reason: 'observe-only (зелёный проход по способности)' };
|
||||
if (!frozenPlan) return { decision: 'block', reason: 'нет замороженного плана — действие вне плана запрещено (default-deny)' };
|
||||
if (!verifyImpl(frozenPlan, key)) return { decision: 'block', reason: 'печать плана невалидна (seal/signature) — требуется заново одобрить план' };
|
||||
const step = nextStep(frozenPlan.steps, stepPtr);
|
||||
if (!step) return { decision: 'block', reason: 'план исчерпан — нет шага под это действие' };
|
||||
// R-08: структурная валидация дерева плана ДО доверия (fail-CLOSED, SE-2/SE-4)
|
||||
if (!validatePlanTree(frozenPlan.steps)) return { decision: 'block', reason: 'структура плана-дерева невалидна (fail-CLOSED)' };
|
||||
// R-08: текущий лист по указателю (целое=depth-1 / массив=дерево); спуск через контейнеры, лист только из sealed-дерева (I2)
|
||||
const step = treeLeafAt(frozenPlan.steps, stepPtr);
|
||||
if (!step) return { decision: 'block', reason: 'план исчерпан / указатель не резолвится в лист' };
|
||||
if (!actionMatchesStep(step, actionOf(toolUse), { normalize }))
|
||||
return { decision: 'block', reason: `действие не в плане (ожидался шаг ${step.n}: ${step.op} ${step.object})` };
|
||||
// Привязка к версии артефакта — только если план опирается на artifact_id:
|
||||
@@ -165,7 +169,14 @@ export function decide({ toolUse, frozenPlan, frozenArtifact = null, stepPtr = 0
|
||||
if (classifyDestructive(actionOf(toolUse).object).floor) {
|
||||
return { decision: 'block', reason: `шаг ${step.n}: разрушительное in-plan действие — пол требует двери владельца (стена не двигает указатель, Δ7)` };
|
||||
}
|
||||
return { decision: 'allow', reason: `шаг ${step.n} плана`, advanceTo: stepPtr + 1 };
|
||||
// R-08: следующая позиция = depth-first ход по дереву, сериализованный (целое для depth-1 / массив).
|
||||
// SE-1: явный флаг advance (runGate гейтит по нему, не по typeof advanceTo).
|
||||
let advanceTo;
|
||||
try {
|
||||
const cur = normalizeToLeaf(frozenPlan.steps, deserializePointer(stepPtr, frozenPlan.steps));
|
||||
advanceTo = serializePointer(advanceOverTree(frozenPlan.steps, cur));
|
||||
} catch { return { decision: 'block', reason: 'навигация по дереву превысила предел (fail-CLOSED)' }; }
|
||||
return { decision: 'allow', reason: `шаг ${step.n} плана`, advance: true, advanceTo };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,7 +224,7 @@ export function runGate({ event, frozenPlan, frozenArtifact, stepPtr, key, verif
|
||||
}
|
||||
return { block: false, message: r.reason };
|
||||
}
|
||||
if (r.decision === 'allow' && typeof r.advanceTo === 'number') {
|
||||
if (r.decision === 'allow' && r.advance === true) {
|
||||
// Δ3 (8.1): пред-запись НАМЕРЕНИЯ в журнал ДО allow (PreToolUse не видит факт исполнения —
|
||||
// честный максимум: «нет записи → нет действия»). Журнал вернул false ИЛИ бросил → стена НЕ
|
||||
// разрешает (block), указатель НЕ двигается. Сверку «произошло ровно записанное» делает
|
||||
|
||||
@@ -479,3 +479,51 @@ describe('R-08: сериализованный указатель (целое И
|
||||
expect(resolve3({ plan_id: 'P1', ptr: 3 }, 'P1')).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ── R-08: decide ходит по дереву; SE-1 явный advance; flat-регрессия ──
|
||||
import { decide as decideTree } from './enforce-supreme-gate.mjs';
|
||||
import { freezePlan as fp4, verifyFrozenPlan as vfp4 } from './plan-lock.mjs';
|
||||
|
||||
const K4 = 'k4';
|
||||
const TREE4 = [
|
||||
{ n: 1, op: 'Write', object: 'tools/a.mjs' },
|
||||
{ n: 2, substeps: [{ n: '2.1', op: 'Edit', object: 'tools/x.mjs' }, { n: '2.2', op: 'Bash', object: 'npx vitest' }] },
|
||||
];
|
||||
const norm4 = (p) => p.toLowerCase();
|
||||
|
||||
describe('R-08 decide — дерево', () => {
|
||||
const plan = fp4({ steps: TREE4, key: K4, nowMs: 1 });
|
||||
it('указатель 0 (целое) → матч первого листа, advance:true', () => {
|
||||
const r = decideTree({ toolUse: { name: 'Write', input: { file_path: 'tools/a.mjs' } }, frozenPlan: plan, stepPtr: 0, key: K4, normalize: norm4 });
|
||||
expect(r.decision).toBe('allow');
|
||||
expect(r.advance).toBe(true);
|
||||
});
|
||||
it('указатель 1 (контейнер) → спуск, матчит лист 2.1, следующий 2.2', () => {
|
||||
const r = decideTree({ toolUse: { name: 'Edit', input: { file_path: 'tools/x.mjs' } }, frozenPlan: plan, stepPtr: 1, key: K4, normalize: norm4 });
|
||||
expect(r.decision).toBe('allow');
|
||||
expect(r.advanceTo).toEqual([1, 1]);
|
||||
});
|
||||
it('действие не лист текущей позиции → block', () => {
|
||||
const r = decideTree({ toolUse: { name: 'Write', input: { file_path: 'tools/evil.mjs' } }, frozenPlan: plan, stepPtr: 0, key: K4, normalize: norm4 });
|
||||
expect(r.decision).toBe('block');
|
||||
});
|
||||
it('контейнер сам действие не матчит (SE-2): Write на метку контейнера → block', () => {
|
||||
const r = decideTree({ toolUse: { name: 'Write', input: { file_path: '2' } }, frozenPlan: plan, stepPtr: 1, key: K4, normalize: norm4 });
|
||||
expect(r.decision).toBe('block');
|
||||
});
|
||||
it('битое дерево (контейнер с op) → block (validatePlanTree)', () => {
|
||||
const bad = fp4({ steps: [{ n: 1, op: 'Write', object: 'a', substeps: [{ n: '1.1', op: 'Edit', object: 'x' }] }], key: K4, nowMs: 1 });
|
||||
const r = decideTree({ toolUse: { name: 'Write', input: { file_path: 'a' } }, frozenPlan: bad, stepPtr: 0, key: K4, normalize: norm4 });
|
||||
expect(r.decision).toBe('block');
|
||||
});
|
||||
});
|
||||
|
||||
describe('R-08 flat-регрессия decide (SE-7)', () => {
|
||||
const flat = fp4({ steps: [{ n: 1, op: 'Write', object: 'tools/foo.mjs' }], key: K4, nowMs: 1 });
|
||||
it('плоский план: advanceTo целое (как раньше), advance:true', () => {
|
||||
const r = decideTree({ toolUse: { name: 'Write', input: { file_path: 'tools/foo.mjs' } }, frozenPlan: flat, stepPtr: 0, key: K4, normalize: norm4 });
|
||||
expect(r.decision).toBe('allow');
|
||||
expect(r.advanceTo).toBe(1);
|
||||
expect(r.advance).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user