bookworm-smart-assistant/scripts/archive/apply-phase2-hook-patches.js

320 lines
14 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.

#!/usr/bin/env node
/**
* Phase 2 Hook Patches 应用脚本 (v6.0 F2-2, F2-4, F2-5)
*
* 由于 block-sensitive-files.js 保护 .claude/hooks/*.js 不允许 AI 直接写入,
* 此脚本由用户手动执行,应用三个 hook 修改:
*
* F2-2: route-interceptor.js — L2 域内精排 (高置信度隔离域外技能)
* F2-4: constitution-precheck.js — 新建 PreToolUse 宪法预检钩子
* F2-5: constitution-guard.js — 扩展 CODE_EXTENSIONS 覆盖多语言
*
* 用法:
* node scripts/apply-phase2-hook-patches.js → 预览 (dry-run)
* node scripts/apply-phase2-hook-patches.js --apply → 应用所有变更
* node scripts/apply-phase2-hook-patches.js --f2-2 → 仅 F2-2
* node scripts/apply-phase2-hook-patches.js --f2-4 → 仅 F2-4
* node scripts/apply-phase2-hook-patches.js --f2-5 → 仅 F2-5
*
* 安全: 应用前备份原文件到 hooks/*.js.bak-phase2
*/
'use strict';
const fs = require('fs');
const path = require('path');
function detectClaudeRoot() {
if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME;
const selfDir = path.dirname(__filename);
if (selfDir.includes('.claude')) return selfDir.replace(/[/\\]scripts$/, '');
return (process.env.USERPROFILE || process.env.HOME || '').replace(/\\/g, '/') + '/.claude';
}
const CLAUDE_ROOT = detectClaudeRoot();
const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks');
// ─── 工具函数 ─────────────────────────────────────────
function readHook(filename) {
const fp = path.join(HOOKS_DIR, filename);
if (!fs.existsSync(fp)) throw new Error('\u6587\u4ef6\u4e0d\u5b58\u5728: ' + fp);
return fs.readFileSync(fp, 'utf8');
}
function writeHook(filename, content) {
const fp = path.join(HOOKS_DIR, filename);
const bakPath = fp + '.bak-phase2';
if (!fs.existsSync(bakPath) && fs.existsSync(fp)) {
fs.copyFileSync(fp, bakPath);
console.log(' \u5df2\u5907\u4efd: ' + bakPath);
}
fs.writeFileSync(fp, content, 'utf8');
console.log(' \u5df2\u5199\u5165: ' + fp);
}
// ─── F2-2: route-interceptor.js L2 域内精排 ──────────
function applyF22(dryRun) {
console.log('\n[F2-2] route-interceptor.js \u2014 L2 \u57df\u5185\u7cbe\u6392');
let content;
try {
content = readHook('route-interceptor.js');
} catch (e) {
console.error(' [ERROR] ' + e.message);
return false;
}
// 文件使用 CRLF — 先规范化再匹配
const hasCRLF = content.includes('\r\n');
const norm = hasCRLF ? content.replace(/\r\n/g, '\n') : content;
// 搜索锚点 (单行CRLF 不影响)
const ANCHOR = ' // v5.9: L1 \u57df\u5916\u964d\u6743 (\u975e\u5019\u9009\u57df\u6280\u80fd\u5206\u6570 \u00d70.5)';
if (!norm.includes(ANCHOR)) {
console.error(' [ERROR] \u672a\u627e\u5230\u9524\u70b9: "v5.9: L1 \u57df\u5916\u964d\u6743"\u3002\u8bf7\u624b\u52a8\u68c0\u67e5 route-interceptor.js \u7b2c244\u884c\u9644\u8fd1\u3002');
return false;
}
// 构建 OLD/NEW (用 join 避免嵌入字面换行)
const NL = '\n';
const OLD = [
' // v5.9: L1 \u57df\u5916\u964d\u6743 (\u975e\u5019\u9009\u57df\u6280\u80fd\u5206\u6570 \u00d70.5)',
' if (candidateSkillSet && finalScore > 0 && !candidateSkillSet.has(skill.name)) {',
' finalScore *= 0.5;',
' }',
].join(NL);
const NEW = [
' // v6.0 F2-2: L2 \u57df\u5185\u7cbe\u6392 \u2014 \u9ad8\u7f6e\u4fe1\u5ea6\u57df\u5206\u7c7b\u65f6\u5b8c\u5168\u9694\u79bb\u57df\u5916\u6280\u80fd',
' // \u5f53 L1 \u7f6e\u4fe1\u5ea6 > 0.6 \u65f6: \u57df\u5916\u6280\u80fd\u76f4\u63a5\u5f52\u96f6 (\u4e0d\u53c2\u4e0e BM25 \u8bc4\u5206\u7ade\u4e89)',
' // \u5f53 L1 \u7f6e\u4fe1\u5ea6 <= 0.6 \u65f6: \u4fdd\u6301\u539f\u6709\u964d\u6743\u7b56\u7565 (\u00d70.5) \u5411\u540e\u517c\u5bb9',
' if (candidateSkillSet && finalScore > 0 && !candidateSkillSet.has(skill.name)) {',
' if (domainInfo && domainInfo.confidence > 0.6) {',
' // \u9ad8\u7f6e\u4fe1\u5ea6: \u5b8c\u5168\u9694\u79bb\u57df\u5916\u6280\u80fd (\u7f6e\u96f6)',
' finalScore = 0;',
' } else {',
' // \u4f4e\u7f6e\u4fe1\u5ea6: \u964d\u6743\u4fdd\u7559 (\u5411\u540e\u517c\u5bb9)',
' finalScore *= 0.5;',
' }',
' }',
].join(NL);
if (!norm.includes(OLD)) {
console.error(' [ERROR] \u5b8c\u6574\u4ee3\u7801\u5757\u4e0d\u5339\u914d\u3002\u8bf7\u624b\u52a8\u5c06 v5.9 \u964d\u6743\u5757\u66ff\u6362\u4e3a v6.0 L2 \u9694\u79bb\u5757\u3002');
return false;
}
let updated = norm.replace(OLD, NEW);
updated = updated.replace(" version: '5.9',", " version: '6.0',");
const final = hasCRLF ? updated.replace(/\n/g, '\r\n') : updated;
if (dryRun) {
console.log(' [DRY-RUN] \u5c06\u5e94\u7528 L2 \u9694\u79bb\u903b\u8f91 | \u884c\u5c3e: ' + (hasCRLF ? 'CRLF' : 'LF') + ' | version: 5.9\u21926.0');
} else {
writeHook('route-interceptor.js', final);
}
return true;
}
// ─── F2-4: constitution-precheck.js 新建 PreToolUse 钩子 ──
function buildPrecheckContent() {
// 逐行构建,避免任何模板字符串或嵌入换行的歧义
const L = [];
L.push('#!/usr/bin/env node');
L.push('/**');
L.push(' * PreToolUse Hook: \u5bbf\u6cd5 ERROR \u7ea7\u89c4\u5219\u9884\u68c0 (v6.0 F2-4)');
L.push(' * Matcher: Write|Edit');
L.push(' *');
L.push(' * constitution-guard.js \u5728 PostToolUse \u963b\u4e0d\u4f4f\u5199\u5165\u524d\u7684\u9519\u8bef\u3002');
L.push(' * \u672c\u9489\u5b50\u5728 PreToolUse \u9636\u6bb5\u5bf9\u6700\u9ad8\u98ce\u9669 ERROR \u89c4\u5219\u8fdb\u884c\u524d\u7f6e\u62e6\u622a\u3002');
L.push(' *');
L.push(' * \u89c4\u5219:');
L.push(' * exec-injection \u2014 child_process \u53c2\u6570\u542b\u53d8\u91cf\u62fc\u63a5');
L.push(' * hardcoded-secret \u2014 \u786c\u7f16\u7801 API Key / Token');
L.push(' *');
L.push(' * \u9000\u51fa\u7801: 0=\u653e\u884c | 2=\u963b\u65ad Fail-open: \u5f02\u5e38\u65f6 exit(0)');
L.push(' */');
L.push("'use strict';");
L.push("const fs = require('fs');");
L.push("const path = require('path');");
L.push('const MAX_STDIN_SIZE = 512 * 1024;');
L.push('try {');
L.push(" const { isEnabled } = require('../scripts/feature-flags.js');");
L.push(" if (!isEnabled('constitution-precheck')) process.exit(0);");
L.push('} catch {}');
L.push('const CODE_EXTENSIONS = /\\.(?:js|ts|jsx|tsx|mjs|cjs|py|go|rs|java|rb|php|sh|bash|zsh|ps1)$/i;');
L.push('const PRECHECK_RULES = [');
L.push(' {');
L.push(" id: 'exec-injection',");
L.push(" label: '\u547d\u4ee4\u6ce8\u5165: child_process \u53c2\u6570\u542b\u53d8\u91cf\u62fc\u63a5',");
// The regex pattern — written as a string value, not as a regex literal to avoid confusing the outer file
L.push(" pattern: /(?:exec|execSync|spawn|spawnSync)\\s*\\(\\s*(?:`[^`]*\\$\\{|['\"][^'\"]*['\"]\\s*\\+)/,");
L.push(' },');
L.push(' {');
L.push(" id: 'hardcoded-secret',");
L.push(" label: '\u786c\u7f16\u7801\u5bc6\u9470: \u5305\u542b\u9ad8\u71b5\u5bc6\u9470\u5b57\u7b26\u4e32',");
L.push(" pattern: /(?:api[_-]?key|api[_-]?secret|access[_-]?token|secret[_-]?key|auth[_-]?token|private[_-]?key)\\s*[=:]\\s*['\"`][A-Za-z0-9+\\/=_\\-]{20,}['\"`]/i,");
L.push(' },');
L.push('];');
L.push('function extractContent(ti) {');
L.push(" return ti ? (ti.content || '') + (ti.new_string || '') : '';");
L.push('}');
L.push('function detectViolation(content) {');
L.push(" const lines = (content || '').split('\\n');");
L.push(' for (const rule of PRECHECK_RULES) {');
L.push(' for (let i = 0; i < lines.length; i++) {');
L.push(' if (rule.pattern && rule.pattern.test(lines[i])) {');
L.push(' return { id: rule.id, label: rule.label, line: i + 1 };');
L.push(' }');
L.push(' }');
L.push(' }');
L.push(' return null;');
L.push('}');
L.push('function logEvent(ruleId, filePath, lineNo) {');
L.push(' try {');
L.push(" const root = path.dirname(__filename).replace(/[\\/\\\\]hooks$/, '');");
L.push(" const dir = path.join(root, 'debug');");
L.push(' if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });');
L.push(' const f = path.join(dir, `security-${new Date().toISOString().slice(0,10)}.jsonl`);');
L.push(" fs.appendFileSync(f, JSON.stringify({ ts: new Date().toISOString(), decision: 'deny', hook: 'constitution-precheck', rule: ruleId, line: lineNo }) + '\\n');");
L.push(' } catch {}');
L.push('}');
L.push('function main() {');
L.push(" let raw = '';");
L.push(" process.stdin.setEncoding('utf8');");
L.push(" process.stdin.on('data', c => { raw += c; if (raw.length > MAX_STDIN_SIZE) process.exit(0); });");
L.push(" process.stdin.on('end', () => {");
L.push(' try {');
L.push(' const input = JSON.parse(raw);');
L.push(" const fp = input.tool_input?.file_path || '';");
L.push(' if (!fp || !CODE_EXTENSIONS.test(fp)) { process.exit(0); return; }');
L.push(' const content = extractContent(input.tool_input);');
L.push(' if (!content) { process.exit(0); return; }');
L.push(' const v = detectViolation(content);');
L.push(' if (v) {');
L.push(' logEvent(v.id, fp, v.line);');
L.push(' process.stderr.write(JSON.stringify({');
L.push(" hookSpecificOutput: { permissionDecision: 'deny' },");
L.push(' systemMessage: [');
L.push(" '[constitution-precheck] ERROR \u7ea7\u8fdd\u89c4\uff0c\u5df2\u963b\u65ad\u5199\u5165',");
L.push(' `\u6587\u4ef6: ${path.basename(fp)}, \u884c: ${v.line}`,');
L.push(' `\u89c4\u5219: [${v.id}] ${v.label}`,');
L.push(" '\u8bf7\u4fee\u590d\u540e\u91cd\u8bd5\u3002\u53c2\u8003: constitution/AI-CONSTITUTION.md \u7b2c\u5341\u4e00\u7ae0',");
L.push(" ].join('\\n'),");
L.push(' }));');
L.push(' process.exit(2); return;');
L.push(' }');
L.push(' process.exit(0);');
L.push(' } catch { process.exit(0); }');
L.push(' });');
L.push('}');
L.push("if (typeof module !== 'undefined') {");
L.push(' module.exports = { PRECHECK_RULES, detectViolation, extractContent, CODE_EXTENSIONS };');
L.push('}');
L.push('if (require.main === module) main();');
return L.join('\n') + '\n';
}
function applyF24(dryRun) {
console.log('\n[F2-4] constitution-precheck.js \u2014 \u65b0\u5efa PreToolUse \u5bbf\u6cd5\u9884\u68c0\u9489\u5b50');
const fp = path.join(HOOKS_DIR, 'constitution-precheck.js');
if (fs.existsSync(fp)) {
console.log(' [SKIP] \u6587\u4ef6\u5df2\u5b58\u5728\uff0c\u8df3\u8fc7\u3002\u5982\u9700\u91cd\u5efa\uff0c\u8bf7\u5148\u5220\u9664\u8be5\u6587\u4ef6\u3002');
return true;
}
const content = buildPrecheckContent();
if (dryRun) {
console.log(' [DRY-RUN] \u5c06\u65b0\u5efa: ' + fp);
console.log(' \u89c4\u5219: exec-injection + hardcoded-secret | \u9636\u6bb5: PreToolUse | \u963b\u65ad\u65f6 exit(2)');
} else {
fs.writeFileSync(fp, content, 'utf8');
console.log(' \u5df2\u521b\u5efa: ' + fp);
}
return true;
}
// ─── F2-5: constitution-guard.js 扩展 CODE_EXTENSIONS ──
function applyF25(dryRun) {
console.log('\n[F2-5] constitution-guard.js \u2014 \u6269\u5c55 CODE_EXTENSIONS');
let content;
try {
content = readHook('constitution-guard.js');
} catch (e) {
console.error(' [ERROR] ' + e.message);
return false;
}
const OLD_EXT = 'const CODE_EXTENSIONS = /\\.(?:js|ts|jsx|tsx|mjs|cjs)$/i;';
const NEW_EXT = 'const CODE_EXTENSIONS = /\\.(?:js|ts|jsx|tsx|mjs|cjs|py|go|rs|java|rb|php|sh|bash|zsh|ps1)$/i;';
if (!content.includes(OLD_EXT)) {
console.error(' [ERROR] \u672a\u627e\u5230 CODE_EXTENSIONS \u5b9a\u4e49\u3002\u624b\u52a8\u5b9a\u4f4d\u7b2c30\u884c\u9644\u8fd1\u5e76\u66ff\u6362\u3002');
console.log(' \u5e94\u6709: ' + OLD_EXT);
console.log(' \u662f: ' + NEW_EXT);
return false;
}
const newContent = content.replace(OLD_EXT, NEW_EXT);
if (dryRun) {
console.log(' [DRY-RUN] \u5c06\u66ff\u6362 CODE_EXTENSIONS:');
console.log(' ' + OLD_EXT);
console.log(' \u2192 ' + NEW_EXT);
console.log(' \u65b0\u589e: .py .go .rs .java .rb .php .sh .bash .zsh .ps1');
} else {
writeHook('constitution-guard.js', newContent);
console.log(' \u65b0\u589e\u8986\u76d6: .py .go .rs .java .rb .php .sh .bash .zsh .ps1');
}
return true;
}
// ─── 主入口 ──────────────────────────────────────────
function main() {
const args = process.argv.slice(2);
const applyMode = args.includes('--apply');
const onlyF22 = args.includes('--f2-2');
const onlyF24 = args.includes('--f2-4');
const onlyF25 = args.includes('--f2-5');
const specificMode = onlyF22 || onlyF24 || onlyF25;
const dryRun = !applyMode;
console.log('=== Phase 2 Hook Patches ===');
console.log('\u6a21\u5f0f: ' + (dryRun ? 'DRY-RUN' : 'APPLY'));
console.log('\u76ee\u6807: ' + HOOKS_DIR + '\n');
const results = {};
if (!specificMode || onlyF22) results.f22 = applyF22(dryRun);
if (!specificMode || onlyF24) results.f24 = applyF24(dryRun);
if (!specificMode || onlyF25) results.f25 = applyF25(dryRun);
console.log('\n=== \u6267\u884c\u6458\u8981 ===');
const labels = { f22: 'F2-2 L2\u7cbe\u6392', f24: 'F2-4 \u9884\u68c0\u9489\u5b50', f25: 'F2-5 \u591a\u8bed\u8a00' };
for (const [k, ok] of Object.entries(results)) {
console.log(' ' + (ok ? '[OK] ' : '[FAIL] ') + (labels[k] || k));
}
if (dryRun) {
console.log('\n\u5e94\u7528: node scripts/apply-phase2-hook-patches.js --apply\n');
return;
}
if (results.f24) {
console.log('\n--- \u540e\u7eed\u6b65\u9aa4: \u5728 settings.json \u6ce8\u518c constitution-precheck.js (PreToolUse Write|Edit) ---');
console.log(' "command": "node ' + path.join(HOOKS_DIR, 'constitution-precheck.js').replace(/\\/g, '/') + '"');
}
console.log('\n\u5b8c\u6210\u3002\u56de\u6eda: \u4f7f\u7528 .bak-phase2 \u6587\u4ef6\u3002');
}
if (require.main === module) main();
module.exports = { applyF22, applyF24, applyF25, buildPrecheckContent };