feat: round-memory owner-seal sealArtifact sealPlan bypass plan-carveout SP3-b3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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) ──
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user