From b111ca5ec100df4097e35c839047cf8f8d883cc1 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: Wed, 17 Jun 2026 03:04:32 +0300 Subject: [PATCH] feat: round-memory owner-seal sealArtifact sealPlan bypass plan-carveout SP3-b3 Co-Authored-By: Claude Opus 4.8 --- tools/seal-orchestration.mjs | 41 ++++++++++++++++++++++--------- tools/seal-orchestration.test.mjs | 32 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/tools/seal-orchestration.mjs b/tools/seal-orchestration.mjs index b03fd1a..c92b847 100644 --- a/tools/seal-orchestration.mjs +++ b/tools/seal-orchestration.mjs @@ -13,7 +13,7 @@ import { buildArtifact } from './artifact-from-spec.mjs'; import { parsePlanSteps } from './plan-steps-parse.mjs'; import { parsePlanSkills } from './plan-skills.mjs'; -import { contentHash, sealOnApproval } from './judge-seal-channel.mjs'; +import { contentHash, sealOnApproval, requiresOwnerSeal } from './judge-seal-channel.mjs'; import { freezeArtifact, freezePlan } from './plan-lock.mjs'; export function sealableArtifact(md) { return buildArtifact(md); } // {sections, source_sha} @@ -22,26 +22,45 @@ export function judgedHashOf(obj) { return contentHash(obj); } function isRealGo(v) { return !!(v && v.wired === true && v.decision === 'GO'); } -/** Печать артефакта на реальном GO (carve-out деньги/тупик — через sealOnApproval). */ -export function sealArtifact({ md, verdict, key, judgeMode, nowMs, freezeImpl = freezeArtifact }) { - if (!isRealGo(verdict)) return { sealed: false, reason: 'нет реального GO (SE-3)' }; +/** Печать артефакта: обычный GO ИЛИ owner-seal (перевешивает NO-GO/деньги/тупик). decideSeal — + * единый «мозг» решения; carve-out деньги/тупик — requiresOwnerSeal (единый детектор, не дублируем). + * judged_hash сверяется на обычном GO; на owner-seal интегрити даёт escape-грант над хешем тела + * (sealTurnProd считает ownerSealOpen над ЭТИМ же телом). */ +export function sealArtifact({ md, verdict, key, judgeMode, nowMs, ownerSealOpen = false, freezeImpl = freezeArtifact }) { const artifact = sealableArtifact(md); - if (verdict.judged_hash !== judgedHashOf(artifact)) return { sealed: false, reason: 'judged_hash mismatch (SD-1/TOCTOU)' }; + const d = decideSeal({ verdict, ownerSealOpen, ownerSealRequired: requiresOwnerSeal({ artifact, verdict }) }); + if (!d.seal) { + return d.ownerRequired + ? { sealed: false, ownerRequired: true, reason: 'деньги/тупик — печать ставит владелец (carve-out §6)' } + : { sealed: false, reason: 'нет реального GO и нет owner-seal (SE-3)' }; + } + if (d.via === 'wired-go' && verdict.judged_hash !== judgedHashOf(artifact)) { + return { sealed: false, reason: 'judged_hash mismatch (SD-1/TOCTOU)' }; + } // judge_mode впрыскивается в печать ПОСЛЕ хеш-сверки sealOnApproval (над чистым artifact). const freezeWithMode = ({ artifact: a, key: k, nowMs: n }) => freezeImpl({ artifact: { ...a, judge_mode: judgeMode }, key: k, nowMs: n }); - return sealOnApproval({ artifact, verdict, key, nowMs, freezeImpl: freezeWithMode }); + return sealOnApproval({ artifact, verdict, key, nowMs, ownerSealOpen, freezeImpl: freezeWithMode }); } -/** Печать плана: штамп artifact_id текущего артефакта (SD-3); нет артефакта → fail-CLOSE (VA-1). */ -export function sealPlan({ md, currentArtifact, verdict, key, judgeMode, nowMs, freezeImpl = freezePlan }) { - if (!isRealGo(verdict)) return { sealed: false, reason: 'нет реального GO (SE-3)' }; +/** Печать плана: штамп artifact_id текущего артефакта (SD-3); нет артефакта → fail-CLOSE (VA-1). + * SP3-b: owner-seal-байпас через decideSeal + денежный carve-out симметрично спеке (§0 — + * detectMoney по телу плана + deadlock вердикта). judged_hash сверяем только на обычном GO. */ +export function sealPlan({ md, currentArtifact, verdict, key, judgeMode, nowMs, ownerSealOpen = false, freezeImpl = freezePlan }) { if (!currentArtifact || !currentArtifact.artifact_id) return { sealed: false, reason: 'нет текущего артефакта (VA-1/SD-3)' }; let planObj; try { planObj = sealablePlan(md); } catch (e) { return { sealed: false, reason: `парс плана fail-CLOSE: ${e.message}` }; } - if (verdict.judged_hash !== judgedHashOf(planObj)) return { sealed: false, reason: 'judged_hash mismatch (SD-1/TOCTOU)' }; + const d = decideSeal({ verdict, ownerSealOpen, ownerSealRequired: requiresOwnerSeal({ artifact: md, verdict }) }); + if (!d.seal) { + return d.ownerRequired + ? { sealed: false, ownerRequired: true, reason: 'деньги/тупик — печать плана ставит владелец (carve-out §6)' } + : { sealed: false, reason: 'нет реального GO и нет owner-seal (SE-3)' }; + } + if (d.via === 'wired-go' && verdict.judged_hash !== judgedHashOf(planObj)) { + return { sealed: false, reason: 'judged_hash mismatch (SD-1/TOCTOU)' }; + } const seal = freezeImpl({ steps: planObj.steps, skills: planObj.skills, artifactId: currentArtifact.artifact_id, judgeMode, key, nowMs }); - return { sealed: true, seal }; + return { sealed: true, seal, via: d.via }; } // ── owner-seal (SP3 / Фикс 3) ── diff --git a/tools/seal-orchestration.test.mjs b/tools/seal-orchestration.test.mjs index 96cff04..f89aea8 100644 --- a/tools/seal-orchestration.test.mjs +++ b/tools/seal-orchestration.test.mjs @@ -70,3 +70,35 @@ describe('owner-seal (SP3)', () => { .toEqual({ seal: true, via: 'owner-seal-carveout' }); }); }); + +describe('owner-seal в seal-функциях (SP3-b3)', () => { + const moneyMd = '## Реш {#dec-a}\nсписание 100 ₽ с баланса'; + it('sealArtifact: NO-GO + ownerSealOpen → печать (override), owner_sealed', () => { + const r = sealArtifact({ md: specMd, verdict: { wired: true, decision: 'NO-GO' }, key: KEY, judgeMode: 'live-block', ownerSealOpen: true }); + expect(r.sealed).toBe(true); + expect(r.seal.owner_sealed).toBe(true); + }); + it('sealArtifact: GO + деньги без owner-seal → ownerRequired (carve-out §6)', () => { + const a = sealableArtifact(moneyMd); + const r = sealArtifact({ md: moneyMd, verdict: goVerdict(a), key: KEY, judgeMode: 'live-block' }); + expect(r.sealed).toBe(false); + expect(r.ownerRequired).toBe(true); + }); + it('sealArtifact: GO + деньги + ownerSealOpen → печать (carveout перевешивает)', () => { + const a = sealableArtifact(moneyMd); + const r = sealArtifact({ md: moneyMd, verdict: goVerdict(a), key: KEY, judgeMode: 'live-block', ownerSealOpen: true }); + expect(r.sealed).toBe(true); + expect(r.seal.owner_sealed).toBe(true); + }); + it('sealPlan: NO-GO + ownerSealOpen + currentArtifact → печать (override)', () => { + const r = sealPlan({ md: planMd, currentArtifact: { artifact_id: 'AID' }, verdict: { wired: true, decision: 'NO-GO' }, key: KEY, judgeMode: 'live-block', ownerSealOpen: true }); + expect(r.sealed).toBe(true); + expect(r.seal.artifact_id).toBe('AID'); + }); + it('sealPlan: GO + deadlock без owner-seal → ownerRequired (новый carve-out плана, §0)', () => { + const planObj = sealablePlan(planMd); + const r = sealPlan({ md: planMd, currentArtifact: { artifact_id: 'AID' }, verdict: { ...goVerdict(planObj), deadlock: true }, key: KEY, judgeMode: 'live-block' }); + expect(r.sealed).toBe(false); + expect(r.ownerRequired).toBe(true); + }); +});