#!/usr/bin/env node /** * L5 修复 — MUST_INVOKE 豁免漏洞 some→every + 移除 meta (2026-04-25) * * 问题: bwr-builder.js L48 用 intents.some(i => EXEMPT.has(i)) 判定豁免。 * 任何复合意图只要含一个豁免词就跳过强制 Skill。 * 结合 'meta' 在 EXEMPT 列表中, "meta+audit"/"meta+debug" 等 * 审计/路由分析场景被全部绕过 MUST_INVOKE_SKILL。 * * 方案: * 1. some → every (全部 intent 都是豁免词才豁免) * 2. 从 EXEMPT 移除 'meta' * 3. 保留 translate/explain/greeting/remember/continue/select/confirm * * 回归预测: * - "你好" [greeting] → 豁免 (every: true) OK * - "翻译这段" [translate] → 豁免 OK * - "顺便看看配置" [meta, general] → 强制 Skill (meta 已移除) OK * - "记住深色主题" [remember] → 豁免 OK * - "解释下原理" [explain, debug] → 强制 Skill (every: false) OK * * 幂等: sentinel `// L5-MUST-INVOKE-EVERY` 已存在则跳过。 * 原子: tmp + rename。备份: .bak.l5-must-invoke. */ 'use strict'; const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const TARGET = path.join(__dirname, '..', 'bwr-builder.js'); const SENTINEL = '// L5-MUST-INVOKE-EVERY'; const BAK_DIR = path.join(__dirname, 'bak'); const APPLY = process.argv.includes('--apply'); function syntaxCheck(file) { execFileSync(process.execPath, ['--check', file], { stdio: 'pipe' }); } function main() { if (!fs.existsSync(TARGET)) { console.error('[L5] bwr-builder.js not found.'); process.exit(2); } let src = fs.readFileSync(TARGET, 'utf8'); if (src.includes(SENTINEL)) { console.log('[L5] sentinel found — already patched, skipping (idempotent).'); process.exit(0); } const OLD_LIST = "const MUST_INVOKE_EXEMPT_INTENTS = new Set(['translate', 'explain', 'greeting', 'meta', 'remember', 'continue', 'select', 'confirm']);"; const NEW_LIST = SENTINEL + " (2026-04-25 L5 修复 — meta 移出豁免, some→every)\n" + "// 真豁免清单: 仅纯翻译/解释/问候/记忆/对话连续意图豁免 MUST_INVOKE_SKILL\n" + "// 'meta' 移除原因: 审计/路由分析常被分类为 meta+x 复合意图, 历史用 some 判定导致豁免逃逸\n" + "const MUST_INVOKE_EXEMPT_INTENTS = new Set(['translate', 'explain', 'greeting', 'remember', 'continue', 'select', 'confirm']);"; if (!src.includes(OLD_LIST)) { console.error('[L5] OLD_LIST not found — abort.'); process.exit(2); } src = src.replace(OLD_LIST, NEW_LIST); const OLD_SOME = '} else if (intents.some(i => MUST_INVOKE_EXEMPT_INTENTS.has(i))) {'; const NEW_EVERY = '} else if (intents.length > 0 && intents.every(i => MUST_INVOKE_EXEMPT_INTENTS.has(i))) {'; if (!src.includes(OLD_SOME)) { console.error('[L5] OLD_SOME branch not found — abort.'); process.exit(2); } src = src.replace(OLD_SOME, NEW_EVERY); if (!fs.existsSync(BAK_DIR)) fs.mkdirSync(BAK_DIR, { recursive: true }); const ts = Date.now(); const bakPath = path.join(BAK_DIR, 'bwr-builder.js.bak.l5-must-invoke.' + ts); if (!APPLY) { console.log('[L5] DRY RUN — pass --apply to write changes.'); console.log(' would backup to: ' + bakPath); return; } const orig = fs.readFileSync(TARGET, 'utf8'); fs.writeFileSync(bakPath, orig); const tmpPath = TARGET + '.tmp.' + ts; fs.writeFileSync(tmpPath, src); fs.renameSync(tmpPath, TARGET); console.log('[L5] applied. backup at ' + bakPath); try { syntaxCheck(TARGET); console.log('[L5] node --check syntax: PASS'); } catch (e) { console.error('[L5] SYNTAX FAILED — rolling back!'); fs.copyFileSync(bakPath, TARGET); process.exit(2); } } main();