397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
3.8 KiB
JavaScript
71 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* guard-block-log — журнал блоков обороны М1–М6 для доски «кто на посту» (D-3).
|
|
* Логгер пишут машинные хуки при РЕШЁННОМ блоке (best-effort, fail-quiet, Node fs —
|
|
* как logVerdictLine/logViolation). Читатели сканируют все session-файлы runtime для
|
|
* глобальной доски (board-генератор не имеет одного session_id). Достоверность журнала —
|
|
* при зарегистрированном поле-страже runtime (Фаза 8); до флипа данных нет (0/0).
|
|
*/
|
|
import fsDefault from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
import { canonicalAction } from './escape-grant.mjs';
|
|
|
|
function defaultRuntimeDir() { return join(homedir(), '.claude', 'runtime'); }
|
|
|
|
/** Чистая запись блока. */
|
|
export function buildGuardBlockEntry({ machine, action, reason, now }) {
|
|
return { ts: now, machine: String(machine ?? ''), action: String(action ?? ''), reason: String(reason ?? '') };
|
|
}
|
|
|
|
/** Best-effort: записать блок машины. action — из canonicalAction(event); sess — из event.session_id.
|
|
* НИКОГДА не бросает (вызывается в block-ветке хука; сбой логгирования не влияет на блок). */
|
|
export function logGuardBlock(event, machine, reason, { fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now() } = {}) {
|
|
try {
|
|
const action = canonicalAction(event && event.tool_name, (event && event.tool_input) || {});
|
|
const sess = (event && event.session_id) || 'unknown';
|
|
const entry = buildGuardBlockEntry({ machine, action, reason, now });
|
|
fsImpl.mkdirSync(runtimeDir, { recursive: true });
|
|
fsImpl.appendFileSync(join(runtimeDir, `guard-blocks-${sess}.jsonl`), JSON.stringify(entry) + '\n');
|
|
} catch { /* fail-quiet */ }
|
|
}
|
|
|
|
function scanSessionFiles(fsImpl, runtimeDir, prefix) {
|
|
let names = [];
|
|
try { names = fsImpl.readdirSync(runtimeDir).filter((f) => f.startsWith(prefix) && f.endsWith('.jsonl')); }
|
|
catch { return []; }
|
|
const out = [];
|
|
for (const name of names) {
|
|
let raw; try { raw = fsImpl.readFileSync(join(runtimeDir, name), 'utf8'); } catch { continue; }
|
|
for (const line of String(raw).split(/\r?\n/)) {
|
|
const t = line.trim(); if (!t) continue;
|
|
let r; try { r = JSON.parse(t); } catch { continue; }
|
|
out.push(r);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function windowSortCap(recs, { now, windowMs, limit }) {
|
|
return recs
|
|
.filter((r) => r && typeof r.ts === 'number' && now - r.ts >= 0 && now - r.ts <= windowMs)
|
|
.sort((a, b) => b.ts - a.ts)
|
|
.slice(0, limit)
|
|
.map((r) => ({ ...r, ts: new Date(r.ts).toISOString() }));
|
|
}
|
|
|
|
/** Недавние блоки машин для доски. */
|
|
export function loadRecentBlocks({ fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now(), windowMs = 86400000, limit = 10 } = {}) {
|
|
const recs = scanSessionFiles(fsImpl, runtimeDir, 'guard-blocks-')
|
|
.map((r) => ({ ts: r.ts, machine: r.machine, action: r.action, reason: r.reason }));
|
|
return windowSortCap(recs, { now, windowMs, limit });
|
|
}
|
|
|
|
/** Недавние escape владельца (floor_escape) для доски. */
|
|
export function loadRecentEscapes({ fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now(), windowMs = 86400000, limit = 10 } = {}) {
|
|
const recs = scanSessionFiles(fsImpl, runtimeDir, 'askuser-decisions-')
|
|
.filter((r) => r && r.type === 'floor_escape' && typeof r.action === 'string')
|
|
.map((r) => ({ ts: typeof r.ts === 'number' ? r.ts : 0, machine: 'escape', action: r.action, reason: 'escape владельца' }));
|
|
return windowSortCap(recs, { now, windowMs, limit });
|
|
}
|