#!/usr/bin/env node /** * blessed-ops (D1, спека 2026-06-18-blessed-ops-runbook-design §3.3) — МОСТ план↔пол для * «благословлённого ops-runbook». Вынесен из enforce-floor отдельным модулем, чтобы СОХРАНИТЬ * Δ9 (пол первее плана: enforce-floor/floor-decide не зависят от plan-lock). Этот мост знает * оба слоя, но зовётся ТОЛЬКО когда владелец открыл ops-runbook-грант — без согласия владельца * пол плана не касается, независимость пола сохранена. * * Предикат blessedOps(cmd) пускает Bash-команду ТОЛЬКО если выполнены ВСЕ условия §3.3: * — открыт ops-runbook: грант на ЭТОТ план; * — план kind:"deploy", печать валидна, judge_mode='live-block' (одобрение к энфорсменту); * — команда ДОСЛОВНО совпадает с Bash-листом опечатанного плана (белый список). * «Ядерный» набор (bashIsFloor) НЕ благословляется здесь — его отсекает floor-decide (§3.4). */ import { homedir } from 'node:os'; import { verifyFrozenPlan, actionMatchesStep, treeLeaves, loadFrozenPlan } from './plan-lock.mjs'; import { opsRunbookGrantOpen, loadOpsRunbookGrants } from './escape-grant.mjs'; import { resolveReceiptKey } from './receipt-key-config.mjs'; /** * Чистое ядро: построить предикат blessedOps(cmd) из опечатанного deploy-плана под открытым * ops-runbook-грантом. null (благословления нет), если: нет плана / нет гранта на этот plan_id / * план не kind:"deploy" / judge_mode≠live-block / печать невалидна / нет Bash-листов. Всё инъектируемо. */ export function buildBlessedOps({ frozenPlan, grants, key, verifyImpl = verifyFrozenPlan, normalize } = {}) { if (!frozenPlan || !frozenPlan.plan_id) return null; if (!opsRunbookGrantOpen(frozenPlan.plan_id, grants)) return null; if (frozenPlan.kind !== 'deploy') return null; if (frozenPlan.judge_mode !== 'live-block') return null; // одобрение к энфорсменту (зеркало SE-2 стены) if (!verifyImpl(frozenPlan, key)) return null; // печать цела const bashLeaves = treeLeaves(frozenPlan.steps || []).filter((s) => s && String(s.op) === 'Bash'); if (bashLeaves.length === 0) return null; const opts = normalize ? { normalize } : {}; return (cmd) => bashLeaves.some((s) => actionMatchesStep(s, { op: 'Bash', object: cmd }, opts)); } /** * I/O (gated): построить blessedOps для сессии. Грузит ops-runbook-гранты ПЕРВЫМ; нет грантов → * null БЕЗ загрузки плана/ключа (Δ9: общий путь пола плана не касается). Есть грант → грузим * опечатанный план + ключ и строим предикат. fsImpl/keyImpl/runtimeDir/loadPlanImpl инъектируемы. */ export function loadBlessedOpsForSession(sessionId, { loadGrantsImpl = loadOpsRunbookGrants, loadPlanImpl = loadFrozenPlan, keyImpl = resolveReceiptKey, runtimeDir = `${homedir()}/.claude/runtime`, } = {}) { const grants = loadGrantsImpl(sessionId); if (!Array.isArray(grants) || grants.length === 0) return null; const frozenPlan = loadPlanImpl({ sessionId, runtimeDir }); const key = keyImpl(); return buildBlessedOps({ frozenPlan, grants, key }); }