bookworm-smart-assistant/scripts/patches/patch-p1-3-fail-mode-lib.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

166 lines
5.0 KiB
JavaScript
Raw 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.

#!/usr/bin/env node
/**
* patch-p1-3-fail-mode-lib.js
*
* P1.3 step1: 创建 hooks/lib/fail-mode.js
*
* 提供 failModeFromFlag(featureName) 决策 API。
* warn 模式(默认 7 天): hook 行为不变,但记录 evolution-log.jsonl violation
* enforce 模式: hook 应主动调用 process.exit(1) 拒绝
*
* 不直接修改 5 处 fail-open hook受 tamper protection
* 仅提供基础设施 + warn 模式仅记录能力。enforce 由用户后续决策。
*/
'use strict';
const fs = require('fs');
const path = require('path');
const TARGET = path.join(__dirname, '..', '..', 'hooks', 'lib', 'fail-mode.js');
const SENTINEL = 'P1-FAIL-MODE-V1';
const CONTENT = `'use strict';
/**
* fail-mode.js — fail-open/fail-closed 决策 API (${SENTINEL})
*
* 红队识别风险: 关键 hook 普遍 fail-open异常即放行→ 攻击者构造慢路径或异常即逃逸守卫。
*
* 设计:
* - feature-flags.json 中读取 features['bookworm.security.failClosed'].mode
* - mode='off' / 不存在: 完全无操作(保留原 fail-open 行为)
* - mode='warn': 记录 evolution-log violation 但放行
* - mode='enforce': 调用方应据此 process.exit(1)
*
* Usage:
* const { failModeDecide } = require('./lib/fail-mode.js');
* try { ... } catch (e) {
* const action = failModeDecide('security-startup-guard', e);
* if (action === 'reject') process.exit(1);
* // 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: '${SENTINEL}' };
`;
function main() {
const dir = path.dirname(TARGET);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
if (fs.existsSync(TARGET)) {
const cur = fs.readFileSync(TARGET, 'utf8');
if (cur.includes(SENTINEL)) {
process.stdout.write('[SKIP] already deployed\n');
process.exit(0);
}
const ts = new Date().toISOString().replace(/[:.]/g, '-');
fs.copyFileSync(TARGET, TARGET + '.bak.' + ts);
process.stdout.write('[BACKUP] ' + TARGET + '.bak.' + ts + '\n');
}
const tmpPath = TARGET + '.tmp.' + process.pid;
fs.writeFileSync(tmpPath, CONTENT);
try {
delete require.cache[require.resolve(tmpPath)];
const mod = require(tmpPath);
// 自检 1: getMode 不抛异常feature-flag 还没加,应返回 'off'
const m = mod.getMode();
if (typeof m !== 'string') throw new Error('getMode return wrong type');
// 自检 2: failModeDecide 不抛异常
const action = mod.failModeDecide('self-test', new Error('test'));
if (!['noop', 'warn', 'reject'].includes(action)) throw new Error('bad action: ' + action);
fs.renameSync(tmpPath, TARGET);
process.stdout.write('[OK] hooks/lib/fail-mode.js deployed\n');
process.stdout.write(' current mode: ' + m + ' (will activate after feature-flag registered)\n');
} catch (e) {
fs.unlinkSync(tmpPath);
process.stderr.write('[ERROR] self-test: ' + e.message + '\n');
process.exit(1);
}
}
if (require.main === module) main();