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>
102 lines
2.9 KiB
JavaScript
102 lines
2.9 KiB
JavaScript
'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' };
|