bbc053e0a6
Деплой, помеченный **Kind:** deploy и опечатанный (наставник+судья GO, judge_mode=live-block), агент выполняет по белому списку шагов под ОДНИМ согласием владельца `FLOOR-ESCAPE: ops-runbook:<plan-hash>` — без аварийного выхода на каждую команду. «Ядерный» набор (rm -rf/force-push/migrate:fresh/ db:wipe) остаётся на per-command escape. - plan-lock: freezePlan принимает kind (в подписанную базу + хеш, как delivery); не-'normal' добавляет поле, обычные планы байт-идентичны старым печатям. - plan-skills: parsePlanKind (**Kind:** deploy|normal, default normal). - seal-orchestration: sealablePlan/sealPlan прокидывают kind в печать. - escape-grant: loadOpsRunbookGrants (окно = существование плана, БЕЗ 5-мин фильтра) + opsRunbookGrantOpen (точный матч на plan_id). - floor-decide: floorDecide получает инъектируемый blessedOps(cmd); content-block команда из набора пропускается, ЯДЕРНЫЙ набор (bashIsFloor) исключён из послабления. - blessed-ops (новый модуль-мост): buildBlessedOps + loadBlessedOpsForSession — знает план+пол, чтобы СОХРАНИТЬ Δ9 (enforce-floor не зависит от модуля печати плана). Предикат пускает команду только дословно из Bash-листов опечатанного deploy-плана. - enforce-floor: gated — blessed-ops грузит план/гранты ТОЛЬКО при открытом ops-runbook-гранте; без согласия владельца пол плана не касается (Δ9 цел). План: docs/superpowers/plans/2026-06-18-blessed-ops-runbook-plan.md Спека: docs/superpowers/specs/2026-06-18-blessed-ops-runbook-design.md §3.1-3.7. +33 теста, свод 4299 passed / 2 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
8.3 KiB
JavaScript
106 lines
8.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* seal-orchestration (C3-C6, §5) — собрать sealable-объект, сверить judged_hash (SD-1),
|
|
* поставить печать ТОЛЬКО на реальном GO (SE-3), со штампом artifact_id (SD-3) и judge_mode.
|
|
* Печать-движок инъектируется (в проде freezeArtifact/freezePlan Машины 2).
|
|
*
|
|
* NB (фикс при сборке, systematic-debugging): judge_mode НЕ входит в судимое представление —
|
|
* judged_hash считается над ЧИСТЫМ sealable-объектом (sealableArtifact/sealablePlan). Поэтому
|
|
* для артефакта judge_mode добавляется в ПЕЧАТЬ через обёртку freezeImpl ПОСЛЕ хеш-сверки
|
|
* sealOnApproval (иначе её внутренний contentHash({...artifact,judge_mode}) разошёлся бы с
|
|
* judged_hash чистого объекта → TOCTOU-false → артефакт никогда не печатался бы).
|
|
*/
|
|
import { buildArtifact } from './artifact-from-spec.mjs';
|
|
import { parsePlanSteps } from './plan-steps-parse.mjs';
|
|
import { parsePlanSkills, parsePlanDelivery, parsePlanKind } from './plan-skills.mjs';
|
|
import { contentHash, sealOnApproval, requiresOwnerSeal } from './judge-seal-channel.mjs';
|
|
import { freezeArtifact, freezePlan, planId } from './plan-lock.mjs';
|
|
|
|
export function sealableArtifact(md) { return buildArtifact(md); } // {sections, source_sha}
|
|
export function sealablePlan(md) { return { steps: parsePlanSteps(md), skills: parsePlanSkills(md), delivery: parsePlanDelivery(md), kind: parsePlanKind(md) }; } // {steps,skills,delivery,kind}
|
|
export function judgedHashOf(obj) { return contentHash(obj); }
|
|
|
|
function isRealGo(v) { return !!(v && v.wired === true && v.decision === 'GO'); }
|
|
|
|
/** Печать артефакта: обычный 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);
|
|
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, ownerSealOpen, freezeImpl: freezeWithMode });
|
|
}
|
|
|
|
/** Печать плана: штамп 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}` }; }
|
|
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, delivery: planObj.delivery, kind: planObj.kind, artifactId: currentArtifact.artifact_id, judgeMode, key, nowMs });
|
|
return { sealed: true, seal, via: d.via };
|
|
}
|
|
|
|
// ── owner-seal (SP3 / Фикс 3) ──
|
|
// Владелец на арбитраже опечатывает артефакт/план как есть, перевешивая NO-GO судьи И
|
|
// наставника. Каноническая строка действия = `owner-seal:<hash>` — её подписывает escape-грант
|
|
// владельца (router-mentor-receipts); контроллер подделать не может (protected runtime).
|
|
|
|
/** Каноническая метка owner-seal для escape-гранта. hash = judged_hash артефакта/плана. */
|
|
export function ownerSealAction(hash) {
|
|
return `owner-seal:${String(hash == null ? '' : hash)}`;
|
|
}
|
|
|
|
/** Решение печати с учётом owner-seal и carve-out §6 (деньги/тупик).
|
|
* - Чистый GO без чувствительной темы → печать обычным путём (wired-go).
|
|
* - Владелец подписал owner-seal:<хеш тела> → печать. Повод различаем для аудита:
|
|
* carveout — судья дал GO, но тема денежная/тупик (§6), владелец подтвердил;
|
|
* override — судья NO-GO/не дозвонился, владелец перевесил.
|
|
* - GO + чувствительная тема, но владелец ещё не подписал → штатно зовём владельца
|
|
* (ownerRequired, carve-out §6) — это ожидаемый путь, не спор.
|
|
* - Ни GO, ни owner-seal → нет печати.
|
|
* ownerSealRequired = requiresOwnerSeal({artifact,verdict}) — единственный детектор «тема
|
|
* чувствительная» (не дублируется здесь). Интеграция owner-seal ↔ requiresOwnerSeal: один
|
|
* механизм подписи owner-seal закрывает И carve-out, И override. */
|
|
export function decideSeal({ verdict, ownerSealOpen = false, ownerSealRequired = false } = {}) {
|
|
if (isRealGo(verdict) && !ownerSealRequired) return { seal: true, via: 'wired-go' };
|
|
if (ownerSealOpen) return { seal: true, via: isRealGo(verdict) ? 'owner-seal-carveout' : 'owner-seal-override' };
|
|
if (isRealGo(verdict) && ownerSealRequired) return { seal: false, via: null, ownerRequired: true };
|
|
return { seal: false, via: null };
|
|
}
|
|
|
|
/** SP3-c: owner-seal-метка для карточки арбитража по содержимому. ТОТ ЖЕ хеш, что считает
|
|
* sealTurnProd → подпись владельца на этой метке сматчится: план (есть steps) → planId(steps);
|
|
* иначе спека → judgedHashOf(sealableArtifact). Тотально — null при сбое (карточка тогда без
|
|
* точной escape-строки, владелец берёт хеш из логов). */
|
|
export function ownerSealActionForContent(content) {
|
|
const c = String(content == null ? '' : content);
|
|
try {
|
|
const steps = sealablePlan(c).steps;
|
|
if (Array.isArray(steps) && steps.length) return ownerSealAction(planId(steps));
|
|
} catch { /* не план — пробуем как спеку */ }
|
|
try { return ownerSealAction(judgedHashOf(sealableArtifact(c))); } catch { return null; }
|
|
}
|