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>
54 lines
3.7 KiB
JavaScript
54 lines
3.7 KiB
JavaScript
#!/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:<plan-hash> грант на ЭТОТ план;
|
|
* — план 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 });
|
|
}
|