From d1ad4e8559321dfa8cd1e711ade8dc42527bebf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 12 Jun 2026 08:45:17 +0300 Subject: [PATCH] feat(m7-phase8): sealedPlanCoversEdit live-wiring + matcher extension to discipline sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit planCoversAction (signed-plan + tree-valid + leaf-match, fail-CLOSED) wires the §6 build-loop differentiator live. main() matcher now fires for tools/enforce-*.mjs (ad-hoc → LAW/escape; under sealed plan → CARD). decide() skips doc-malice prose layers for code and allows build-loop CARD (M2/content-floor/TDD govern). Hook inert until Phase 8 registration. +10 tests; regression tools-only 3397 passed / 2 skip. Plan: docs/superpowers/plans/2026-06-08-router-mentor-machine-7-phase-5.md (Deferred Ф8). --- tools/enforce-normative-content-rules.mjs | 73 +++++++++++++++---- .../enforce-normative-content-rules.test.mjs | 67 +++++++++++++++++ 2 files changed, 126 insertions(+), 14 deletions(-) diff --git a/tools/enforce-normative-content-rules.mjs b/tools/enforce-normative-content-rules.mjs index 8aa09894..7f5a5135 100644 --- a/tools/enforce-normative-content-rules.mjs +++ b/tools/enforce-normative-content-rules.mjs @@ -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), diff --git a/tools/enforce-normative-content-rules.test.mjs b/tools/enforce-normative-content-rules.test.mjs index fa57325f..c92536d0 100644 --- a/tools/enforce-normative-content-rules.test.mjs +++ b/tools/enforce-normative-content-rules.test.mjs @@ -282,3 +282,70 @@ describe('decide §6 — ЗАКОН требует escape, КАРТА — ски expect(r.reason).toMatch(/fake.?rule/i); }); }); + +import { planCoversAction } from './enforce-normative-content-rules.mjs'; +import { freezePlan } from './plan-lock.mjs'; + +describe('planCoversAction — запечатанный план покрывает правку дисциплинарного исходника (Ф8 live-wiring)', () => { + const steps = [{ n: 1, op: 'Edit', object: 'tools/enforce-floor.mjs' }]; + const action = { op: 'Edit', object: 'tools/enforce-floor.mjs' }; + + it('нет замороженного плана → false (консервативно: ad-hoc → ЗАКОН)', () => { + expect(planCoversAction({ frozenPlan: null, key: 'k', action })).toBe(false); + }); + it('печать невалидна → false (фикция без печати)', () => { + expect(planCoversAction({ frozenPlan: { steps }, key: 'k', action, verifyImpl: () => false })).toBe(false); + }); + it('структура дерева невалидна → false (fail-CLOSED, SE-4 пустой substeps)', () => { + const bad = { steps: [{ substeps: [] }] }; + expect(planCoversAction({ frozenPlan: bad, key: 'k', action, verifyImpl: () => true })).toBe(false); + }); + it('лист совпадает с действием при валидной печати → true (build-loop CARD)', () => { + expect(planCoversAction({ frozenPlan: { steps }, key: 'k', action, verifyImpl: () => true })).toBe(true); + }); + it('ни один лист не совпал → false', () => { + const other = { op: 'Edit', object: 'tools/enforce-supreme-gate.mjs' }; + expect(planCoversAction({ frozenPlan: { steps }, key: 'k', action: other, verifyImpl: () => true })).toBe(false); + }); + it('реальная печать freezePlan + правильный ключ → true; неверный ключ → false (end-to-end seal)', () => { + const plan = freezePlan({ steps, key: 'secret-key', nowMs: 1000 }); + expect(planCoversAction({ frozenPlan: plan, key: 'secret-key', action })).toBe(true); + expect(planCoversAction({ frozenPlan: plan, key: 'wrong-key', action })).toBe(false); + }); +}); + +describe('decide build-loop §6 — дисциплинарный исходник под/вне печати (live sealedPlanCoversEdit)', () => { + it('дисциплинарный исходник ПОД печатью → block:false без claude-md-management (M2/content-floor/TDD govern)', async () => { + const r = await decide({ + filePath: 'tools/enforce-floor.mjs', content: 'обычная правка кода', skillActive: false, + sealedPlanCoversEdit: true, + }); + expect(r.block).toBe(false); + }); + it('дисциплинарный исходник ВНЕ печати → block (ЗАКОН, требует escape владельца)', async () => { + const r = await decide({ + filePath: 'tools/enforce-floor.mjs', content: 'обычная правка кода', skillActive: true, + sealedPlanCoversEdit: false, + multiJudgeImpl: async () => ({ decision: 'NO' }), + }); + expect(r.block).toBe(true); + expect(r.reason).toMatch(/escape|ЗАКОН|§6/i); + }); + it('дисциплинарный исходник ВНЕ печати но с escape владельца → block:false', async () => { + const action = canonicalAction('Edit', { file_path: 'tools/enforce-floor.mjs' }); + const r = await decide({ + filePath: 'tools/enforce-floor.mjs', content: 'обычная правка кода', skillActive: false, + sealedPlanCoversEdit: false, + escapeAction: action, escapeGrants: [{ action, ts: 999 }], escapeConsumed: [], now: 1000, + }); + expect(r.block).toBe(false); + }); + it('дисциплинарный исходник под печатью с командой-строкой в коде (gate-config.json) → block:false (doc-malice слои не применяются к коду)', async () => { + const r = await decide({ + filePath: 'tools/enforce-floor.mjs', + content: "const P = /gate-config\\.json/i; // legit code mentioning a path", + skillActive: false, sealedPlanCoversEdit: true, + }); + expect(r.block).toBe(false); + }); +});