bookworm-smart-assistant/hooks/lib/fail-mode.js
Bookworm Admin 34f304881f fix: strip session-continuity-mcp hooks from Portable template
export.mjs now removes hooks referencing npm packages not included
in the Portable distribution (session-continuity-mcp).
Eliminates MODULE_NOT_FOUND errors on Portable installations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:15:39 +08:00

102 lines
2.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
/**
* fail-mode.js — fail-open/fail-closed 决策 API (P1-FAIL-MODE-V1)
*
* 红队识别风险: 关键 hook 普遍 fail-open异常即放行→ 攻击者构造慢路径或异常即逃逸守卫。
*
* 设计:
* - feature-flags.json 中读取 features['bookworm.security.failClosed'].mode
* - mode='off' / 不存在: 完全无操作(保留原 fail-open 行为)
* - mode='warn': 记录 evolution-log violation 但放行
* - mode='enforce': 调用方应据此 process.exit(2)
*
* Usage:
* const { failModeDecide } = require('./lib/fail-mode.js');
* try { ... } catch (e) {
* const action = failModeDecide('security-startup-guard', e);
* if (action === 'reject') process.exit(2);
* // else: 原 fail-open 路径
* }
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..', '..');
const FLAGS = path.join(ROOT, 'feature-flags.json');
const EVOLUTION_LOG = path.join(ROOT, 'evolution-log.jsonl');
const FLAG_FEATURE = 'bookworm.security.failClosed';
let _cachedFlags = null;
let _cacheMtime = 0;
function loadFlags() {
try {
const stat = fs.statSync(FLAGS);
if (_cachedFlags && stat.mtimeMs === _cacheMtime) return _cachedFlags;
const raw = JSON.parse(fs.readFileSync(FLAGS, 'utf8'));
_cachedFlags = raw && raw.features ? raw.features : {};
_cacheMtime = stat.mtimeMs;
return _cachedFlags;
} catch (_) {
return {};
}
}
/**
* 决策 API
* @param {string} hookName - 调用方标识,如 'security-startup-guard'
* @param {Error|object} ctx - 异常或上下文
* @returns {'noop'|'warn'|'reject'}
*/
function failModeDecide(hookName, ctx) {
const flags = loadFlags();
const cfg = flags[FLAG_FEATURE];
const mode = cfg && cfg.mode ? cfg.mode : 'off';
if (mode === 'off' || !cfg || cfg.enabled === false) return 'noop';
if (mode === 'warn') {
// 仅记录,不拒绝
try {
const entry = {
ts: new Date().toISOString(),
type: 'failmode.violation',
hook: hookName,
ctxMessage: ctx && ctx.message ? String(ctx.message).slice(0, 200) : null,
mode: 'warn',
};
fs.appendFileSync(EVOLUTION_LOG, JSON.stringify(entry) + '\n');
} catch (_) { /* best effort */ }
return 'warn';
}
if (mode === 'enforce') {
try {
const entry = {
ts: new Date().toISOString(),
type: 'failmode.rejected',
hook: hookName,
ctxMessage: ctx && ctx.message ? String(ctx.message).slice(0, 200) : null,
mode: 'enforce',
};
fs.appendFileSync(EVOLUTION_LOG, JSON.stringify(entry) + '\n');
} catch (_) {}
return 'reject';
}
return 'noop';
}
/**
* 当前 mode 查询
*/
function getMode() {
const flags = loadFlags();
const cfg = flags[FLAG_FEATURE];
if (!cfg || cfg.enabled === false) return 'off';
return cfg.mode || 'off';
}
module.exports = { failModeDecide, getMode, __sentinel: 'P1-FAIL-MODE-V1' };