diff --git a/tools/judge-seal-channel.mjs b/tools/judge-seal-channel.mjs index e0d8f2d..12ffdb0 100644 --- a/tools/judge-seal-channel.mjs +++ b/tools/judge-seal-channel.mjs @@ -47,10 +47,21 @@ export function requiresOwnerSeal({ artifact, verdict }) { * @param {Function} a.freezeImpl - ({artifact,key,nowMs}) => sealed-object. ОБЯЗАТЕЛЕН; * в проде оркестрация 4-E передаёт freezeArtifact Машины 2 (ESM-импортом, не require). */ -export function sealOnApproval({ artifact, verdict, key, nowMs, freezeImpl }) { +export function sealOnApproval({ artifact, verdict, key, nowMs, freezeImpl, ownerSealOpen = false }) { if (typeof freezeImpl !== 'function') { return { sealed: false, reason: 'freezeImpl не передан — печать-движок Машины 2 не подключён' }; } + // SP3-b (owner-seal): владелец подписал owner-seal:<хеш ЭТОГО тела> (escape-грант) → печать + // минуя GO/судью/carve-out. hash-интегрити обеспечен совпадением гранта над contentHash(artifact) + // в sealTurnProd (тот же объект — подмена тела ломает совпадение гранта). Штамп owner_sealed. + if (ownerSealOpen) { + const ownerHash = contentHash(artifact); + const seal = freezeImpl({ + artifact: { ...artifact, judged_by: (verdict && verdict.verdict_id) ?? null, judged_hash: ownerHash, owner_sealed: true }, + key, nowMs, + }); + return { sealed: true, seal, via: 'owner-seal' }; + } if (!verdict || verdict.decision !== 'GO') { return { sealed: false, reason: 'нет GO судьи — печать не ставится' }; } diff --git a/tools/judge-seal-channel.test.mjs b/tools/judge-seal-channel.test.mjs index bac37c8..822d20f 100644 --- a/tools/judge-seal-channel.test.mjs +++ b/tools/judge-seal-channel.test.mjs @@ -37,6 +37,23 @@ describe('sealOnApproval (K1 печать только по одобрению + expect(r.sealed).toBe(false); expect(r.ownerRequired).toBe(true); }); + it('SP3-b: ownerSealOpen → печать минуя GO (override NO-GO), штамп owner_sealed', () => { + const r = sealOnApproval({ artifact: ART, verdict: { decision: 'NO-GO' }, key: 'k', freezeImpl: stubFreeze, ownerSealOpen: true }); + expect(r.sealed).toBe(true); + expect(r.seal.owner_sealed).toBe(true); + expect(r.via).toBe('owner-seal'); + }); + it('SP3-b: ownerSealOpen перевешивает денежный carve-out → печать ставится', () => { + const moneyArt = { sections: { '§1': 'списание 100 ₽ с баланса' }, goal: 'биллинг' }; + const r = sealOnApproval({ artifact: moneyArt, verdict: verdict(moneyArt), key: 'k', freezeImpl: stubFreeze, ownerSealOpen: true }); + expect(r.sealed).toBe(true); + expect(r.seal.owner_sealed).toBe(true); + }); + it('SP3-b: ownerSealOpen без вердикта (degraded) → печать, judged_hash = хеш тела', () => { + const r = sealOnApproval({ artifact: ART, verdict: null, key: 'k', freezeImpl: stubFreeze, ownerSealOpen: true }); + expect(r.sealed).toBe(true); + expect(r.seal.judged_hash).toBe(contentHash(ART)); + }); }); describe('detectMoney (осторожный детерминированный детектор)', () => {