|
|
|
@@ -159,17 +159,26 @@ export async function decide({ filePath, content, skillActive, protectiveSubset
|
|
|
|
|
if (escapeAction && escapeGrantOpen(escapeAction, escapeGrants, escapeConsumed, now)) {
|
|
|
|
|
return { block: false, reason: 'normative: escaped owner law-edit (§6)' };
|
|
|
|
|
}
|
|
|
|
|
// Контент-слои (defense-in-depth) — бегут для ВСЕЙ нормативки ДО classification, чтобы
|
|
|
|
|
// вредный контент ловился конкретным reason'ом. «Память = совет, НИКОГДА не разрешение».
|
|
|
|
|
// Layer 1 — recovery keywords.
|
|
|
|
|
const rec = hasRecoveryPattern(content);
|
|
|
|
|
if (rec.flagged) return { block: true, reason: 'recovery-pattern in normative content (layer 1)' };
|
|
|
|
|
// Layer 3 — suspicious feedback.
|
|
|
|
|
const sus = hasSuspiciousFeedback(content);
|
|
|
|
|
if (sus.flagged) return { block: true, reason: 'suspicious self-authorization feedback in normative content (layer 3)' };
|
|
|
|
|
// Layer 2 — fake-rule formulation.
|
|
|
|
|
const fake = hasFakeRuleClaim(content);
|
|
|
|
|
if (fake.flagged) return { block: true, reason: 'fake-rule permission claim in normative content (layer 2)' };
|
|
|
|
|
|
|
|
|
|
// M7 Ф8 (§6): дисциплинарный исходник (tools/enforce-*.mjs и пр.) — это КОД, не проза.
|
|
|
|
|
// doc-malice слои (recovery/fake-rule/suspicious) — детекторы прозы нормативных ДОКУМЕНТОВ;
|
|
|
|
|
// к коду неприменимы (легитимный код упоминает gate-config.json / settings.json / rm). Для кода
|
|
|
|
|
// решает только КАРТА/ЗАКОН: под печатью → CARD (M2/content-floor/TDD govern); вне → LAW (escape).
|
|
|
|
|
const disciplineSource = isDisciplineSourcePath(filePath);
|
|
|
|
|
|
|
|
|
|
if (!disciplineSource) {
|
|
|
|
|
// Контент-слои (defense-in-depth) — бегут для нормативных ДОКУМЕНТОВ ДО classification, чтобы
|
|
|
|
|
// вредный контент ловился конкретным reason'ом. «Память = совет, НИКОГДА не разрешение».
|
|
|
|
|
// Layer 1 — recovery keywords.
|
|
|
|
|
const rec = hasRecoveryPattern(content);
|
|
|
|
|
if (rec.flagged) return { block: true, reason: 'recovery-pattern in normative content (layer 1)' };
|
|
|
|
|
// Layer 3 — suspicious feedback.
|
|
|
|
|
const sus = hasSuspiciousFeedback(content);
|
|
|
|
|
if (sus.flagged) return { block: true, reason: 'suspicious self-authorization feedback in normative content (layer 3)' };
|
|
|
|
|
// Layer 2 — fake-rule formulation.
|
|
|
|
|
const fake = hasFakeRuleClaim(content);
|
|
|
|
|
if (fake.flagged) return { block: true, reason: 'fake-rule permission claim in normative content (layer 2)' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// M7 Фаза 5 (§6): classification КАРТА/ЗАКОН. ЗАКОН (Pravila/PSR/Tooling + дисциплинарный
|
|
|
|
|
// исходник ВНЕ плана + контент-правка правил) требует escape владельца — скил недостаточен
|
|
|
|
@@ -179,7 +188,15 @@ export async function decide({ filePath, content, skillActive, protectiveSubset
|
|
|
|
|
return { block: true, reason: `§6: правка ЗАКОНА (${cls.reason}) требует escape владельца — скил недостаточен` };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// КАРТА — claude-md-management как канал operational-карты.
|
|
|
|
|
// M7 Ф8 (§6): CARD дисциплинарного исходника = build-loop под ЗАПЕЧАТАННЫМ планом. Авторизован
|
|
|
|
|
// самим планом — стена М2 enforce-ит членство шага, content-floor рубит опасные команды, TDD/
|
|
|
|
|
// criterion-gate держат качество. doc-skill (claude-md-management) и doc-судья здесь не применяются
|
|
|
|
|
// (это код, а не документ-карта). До CARD дошёл только plan-covered случай (ad-hoc → LAW выше).
|
|
|
|
|
if (disciplineSource) {
|
|
|
|
|
return { block: false, reason: 'discipline source under sealed plan (build-loop §6)' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// КАРТА-ДОКУМЕНТ — claude-md-management как канал operational-карты.
|
|
|
|
|
// Layer 5 — legit skill must be active.
|
|
|
|
|
if (!skillActive) {
|
|
|
|
|
return { block: true, reason: 'normative write without an active legit skill (claude-md-management) — direct bypass attempt' };
|
|
|
|
@@ -208,6 +225,21 @@ import { homedir } from 'node:os';
|
|
|
|
|
import { readStdin, parseEventJson, readTranscript, turnToolUses, exitDecision } from './enforce-hook-helpers.mjs';
|
|
|
|
|
import { multiJudgeConsensus, JUDGE_MODELS } from './llm-judge.mjs';
|
|
|
|
|
import { canonicalAction, escapeGrantOpen, loadFloorEscapes, loadConsumed } from './escape-grant.mjs';
|
|
|
|
|
import { verifyFrozenPlan, validatePlanTree, treeLeaves, actionMatchesStep, loadFrozenPlan } from './plan-lock.mjs';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* §6 build-loop live-wiring (Ф8): покрыта ли правка дисциплинарного исходника шагом
|
|
|
|
|
* ЗАПЕЧАТАННОГО плана? Фикция без печати — считается только подписанный план (verifyImpl).
|
|
|
|
|
* Совпадение с ЛЮБЫМ листом дерева (порядок шагов держит стена М2 отдельно). Любой провал
|
|
|
|
|
* (нет плана / битая печать / битая структура / нет совпадения) → false → консервативно ЗАКОН.
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
export function planCoversAction({ frozenPlan, key, action, verifyImpl = verifyFrozenPlan, normalize } = {}) {
|
|
|
|
|
if (!frozenPlan || !action) return false;
|
|
|
|
|
if (!verifyImpl(frozenPlan, key)) return false;
|
|
|
|
|
if (!validatePlanTree(frozenPlan.steps).ok) return false;
|
|
|
|
|
return treeLeaves(frozenPlan.steps).some((s) => actionMatchesStep(s, action, { normalize }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** True if any tool use in the turn is a legit normative-edit skill. */
|
|
|
|
|
export function detectLegitSkillActive(toolUses) {
|
|
|
|
@@ -239,7 +271,9 @@ async function main() {
|
|
|
|
|
const event = parseEventJson(await readStdin());
|
|
|
|
|
const toolName = event.tool_name;
|
|
|
|
|
const filePath = event.tool_input && event.tool_input.file_path;
|
|
|
|
|
if (!isNormativePath(filePath)) { exitDecision({ block: false }); return; }
|
|
|
|
|
// M7 Ф8 (§6): matcher расширен с нормативных ДОКУМЕНТОВ на дисциплинарные исходники машин —
|
|
|
|
|
// ad-hoc правка tools/enforce-*.mjs ловится как ЗАКОН (требует escape), build-loop под планом → CARD.
|
|
|
|
|
if (!isNormativePath(filePath) && !isDisciplineSourcePath(filePath)) { exitDecision({ block: false }); return; }
|
|
|
|
|
|
|
|
|
|
const content = extractWrittenContent(toolName, event.tool_input);
|
|
|
|
|
const transcript = readTranscript(event.transcript_path);
|
|
|
|
@@ -247,10 +281,21 @@ async function main() {
|
|
|
|
|
const sessionId = event.session_id;
|
|
|
|
|
const escapeAction = canonicalAction(event.tool_name, event.tool_input || {}); // §6 escape binding-key
|
|
|
|
|
|
|
|
|
|
// M7 Ф8 (§6) live-wiring: дисциплинарный исходник под ЗАПЕЧАТАННЫМ планом → CARD (build-loop).
|
|
|
|
|
// fail-CLOSED: нет ключа/плана/совпадения (или любая ошибка) → false → ЗАКОН (требует escape).
|
|
|
|
|
let sealedPlanCoversEdit = false;
|
|
|
|
|
try {
|
|
|
|
|
const { resolveReceiptKey } = await import('./receipt-key-config.mjs');
|
|
|
|
|
const key = resolveReceiptKey();
|
|
|
|
|
const runtimeDir = `${homedir()}/.claude/runtime`;
|
|
|
|
|
const frozenPlan = loadFrozenPlan({ sessionId, runtimeDir });
|
|
|
|
|
sealedPlanCoversEdit = planCoversAction({ frozenPlan, key, action: { op: toolName, object: filePath } });
|
|
|
|
|
} catch { sealedPlanCoversEdit = false; }
|
|
|
|
|
|
|
|
|
|
const result = await decide({
|
|
|
|
|
filePath, content, skillActive,
|
|
|
|
|
protectiveSubset: isProtectiveNormativePath(filePath), // 7.2: degraded-судья → fail-CLOSE для подмножества
|
|
|
|
|
sealedPlanCoversEdit: false, // §6 build-loop: Ф8 live-wiring через plan-lock actionMatchesStep; пока консервативно false
|
|
|
|
|
sealedPlanCoversEdit, // §6 build-loop: Ф8 live-wiring через plan-lock actionMatchesStep
|
|
|
|
|
escapeAction,
|
|
|
|
|
escapeGrants: loadFloorEscapes(sessionId),
|
|
|
|
|
escapeConsumed: loadConsumed(sessionId),
|
|
|
|
|