#!/usr/bin/env node /** * PreToolUse Hook: Skill 合规校验门控 (v5.2 Neural Gateway) * Matcher: Skill * * 校验 Skill 调用是否在路由推荐集合内。 * 不匹配 → exit(2) 拦截 + 注入纠正提示。 * * stdin: { tool_name: "Skill", tool_input: { skill, args } } * 退出码: 0=放行, 2=拦截(deny) * * Fail-close: 解析异常 → exit(2) ask (安全组件不可因故障静默放行) */ const fs = require('fs'); const path = require('path'); const { safeAppendJsonl } = require('./lib/safe-append.js'); const readStdin = require('./lib/read-stdin.js'); const CLAUDE_ROOT = require('./lib/root.js'); const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug'); const STATE_FILE = path.join(DEBUG_DIR, 'route-state-current.json'); const os = require('os'); const DISK_CACHE_FILE = require('path').join((function(){ try { return require('../scripts/paths.config.js').PATHS.root; } catch { return require('path').join(process.env.USERPROFILE || process.env.HOME, '.claude'); } })(), 'debug', '.disk-cache.json'); const DISK_CACHE_TTL = 5 * 60 * 1000; // 5 分钟缓存 TTL /** * 磁盘空间断路器: 可用空间 < 100MB 时返回 true * 防止磁盘满导致状态文件写入失败 → 安全门控级联失效 * v5.9: 优先 PowerShell Get-CimInstance,fallback wmic (兼容旧系统) */ function isDiskCritical() { // 先检查磁盘检测缓存(5 分钟有效) try { if (require('fs').existsSync(DISK_CACHE_FILE)) { const cache = JSON.parse(require('fs').readFileSync(DISK_CACHE_FILE, 'utf8')); if (Date.now() - cache.ts < DISK_CACHE_TTL) return cache.critical; } } catch {} const THRESHOLD = 100 * 1024 * 1024; // 100MB let result = false; try { const { spawnSync } = require('child_process'); const drive = CLAUDE_ROOT.charAt(0) + ':'; // XC2 修复: driveLetter 输入校验,防止命令注入 const driveLetter = drive.charAt(0); if (!/^[A-Za-z]$/.test(driveLetter)) { return false; // 非法驱动器字母,默认不阻断 } // 优先 PowerShell Get-PSDrive (Windows 10/11 通用,无弃用风险) let psSuccess = false; try { const psResult = spawnSync('powershell', ['-NoProfile', '-Command', `(Get-PSDrive ${driveLetter}).Free`], { encoding: 'utf8', timeout: 3000 }); const psOut = (psResult.stdout || '').trim(); const freeSpace = parseInt(psOut, 10); if (!isNaN(freeSpace)) { result = freeSpace < THRESHOLD; psSuccess = true; // XC2 修复: PowerShell 成功后立即写缓存并返回,不再执行 wmic try { require('fs').writeFileSync(DISK_CACHE_FILE, JSON.stringify({ ts: Date.now(), critical: result })); } catch {} return result; } } catch {} // XC2 修复: Fallback wmic 仅在 PowerShell 失败时执行 if (!psSuccess) { const wmicResult = spawnSync('wmic', ['logicaldisk', 'where', `DeviceID='${driveLetter}:'`, 'get', 'FreeSpace', '/value'], { encoding: 'utf8', timeout: 2000 }); const wmicOut = wmicResult.stdout || ''; const match = wmicOut.match(/FreeSpace=(\d+)/); if (match) { result = parseInt(match[1], 10) < THRESHOLD; } } } catch {} // 写入缓存(无论检测成功还是失败默认 false 都缓存) try { require('fs').writeFileSync(DISK_CACHE_FILE, JSON.stringify({ ts: Date.now(), critical: result })); } catch {} return result; // 检测失败时 result=false,不触发断路器 } /** * 加载路由状态 * @returns {Object|null} */ function loadRouteState(currentSessionId) { try { if (!fs.existsSync(STATE_FILE)) return null; return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } catch { return null; } } /** * 构建允许的技能集合 * @param {Object} state - route-state-current.json * @param {string} prompt - 原始用户 prompt (用于检测显式调用) * @returns {Set} */ function buildAllowedSet(state, prompt) { const allowed = new Set(); // 主路由 if (state.routing?.primary) { allowed.add(state.routing.primary); } // 候选 (置信度 ≥ 0.3) for (const c of (state.routing?.candidates || [])) { if (c.confidence >= 0.3) { allowed.add(c.name); } } // 技能链中所有技能 for (const s of (state.routing?.chain || [])) { allowed.add(s); } // 始终允许 developer-expert (通用回退) allowed.add('developer-expert'); // 用户 prompt 中显式调用 /skill-name → 放行 if (prompt) { const explicitMatch = prompt.match(/^\/([\w-]+)/); if (explicitMatch) { allowed.add(explicitMatch[1]); } } return allowed; } /** * 记录 compliance 事件 * @param {Object} entry */ function logCompliance(entry) { try { const dateStr = new Date().toISOString().slice(0, 10); const logFile = path.join(DEBUG_DIR, `compliance-${dateStr}.jsonl`); safeAppendJsonl(logFile, entry); } catch (e) { // v5.9: 记录日志写入失败到 stderr (可通过 hook-errors.log 追踪) try { process.stderr.write(`[compliance-gate] log write failed: ${e.message}\n`); } catch {} } } // === 主流程 === function main() { readStdin({ maxSize: 128 * 1024 }).then(input => { const skillName = input.tool_input?.skill; if (!skillName) { process.exit(0); return; } // === Phase 0: 逃生舱覆盖检查 === try { const userOverrides = require('../scripts/user-overrides.js'); // /force 检查: 放行 + 记录 + 清除 force const forceState = userOverrides.isForceActive(); if (forceState.active) { logCompliance({ ts: new Date().toISOString(), traceId: 'override', event: 'gate-force-bypass', skill: skillName, override: 'force', forceSkill: forceState.skill || null, }); userOverrides.clearForce(); // 单次生效 process.exit(0); return; } // /checks off 检查: 直接放行 if (userOverrides.isChecksDisabled()) { logCompliance({ ts: new Date().toISOString(), traceId: 'override', event: 'gate-checks-bypass', skill: skillName, override: 'checksOff', }); process.exit(0); return; } } catch (e) { // v5.9: 记录 user-overrides 加载失败 (模块不存在是预期情况,其他错误需排查) if (e && e.code !== 'MODULE_NOT_FOUND') { try { process.stderr.write(`[compliance-gate] user-overrides error: ${e.message}\n`); } catch {} } } // v5.9: 磁盘空间断路器 — 防止状态文件不可写导致安全门控级联失效 if (isDiskCritical()) { logCompliance({ ts: new Date().toISOString(), traceId: 'circuit-breaker', event: 'gate-disk-critical', skill: skillName, }); process.stderr.write(JSON.stringify({ hookSpecificOutput: { permissionDecision: 'ask' }, systemMessage: '[合规门控] 磁盘空间不足(<100MB),安全门控无法正常工作,请用户确认操作。', })); process.exit(2); return; } // 加载路由状态 const state = loadRouteState(); // 无 route-state → 放行 (可能是非用户消息触发的 Skill 调用) if (!state) { process.exit(0); return; } // simple 复杂度 → 放行 (不需要路由) if (state.intent?.complexity === 'simple') { process.exit(0); return; } // 构建允许集合 (传递 promptRaw 用于检测显式 /skill-name 调用) const allowed = buildAllowedSet(state, state.promptRaw || ''); // 校验 if (allowed.has(skillName)) { // 合规: 放行并记录 logCompliance({ ts: new Date().toISOString(), traceId: state.traceId, event: 'gate-pass', skill: skillName, primary: state.routing?.primary, compliant: true, }); // v5.9: 去耦合 — 不再回写 route-state-current.json (消除共享可变状态) // actualSkill 记录到独立的 compliance 日志中,供 route-auditor 通过 traceId 关联 try { // H1 修复: append-only jsonl 防 TOCTOU 污染 (原 actual-skill.json 单文件 lastWriter-wins) const actualJsonl = path.join(DEBUG_DIR, 'actual-skills.jsonl'); const line = JSON.stringify({ traceId: state.traceId, actualSkill: skillName, ts: new Date().toISOString(), }) + '\n'; fs.appendFileSync(actualJsonl, line); // W2 (2026-04-16): 旧 actual-skill.json 默认不写,由 feature flag 恢复 // 回滚: set BOOKWORM_LEGACY_ACTUAL_SKILL=1 if (process.env.BOOKWORM_LEGACY_ACTUAL_SKILL === '1') { const actualFile = path.join(DEBUG_DIR, 'actual-skill.json'); fs.writeFileSync(actualFile, line); } } catch {} process.exit(0); return; } // 不合规: 拦截 const primary = state.routing?.primary || 'developer-expert'; logCompliance({ ts: new Date().toISOString(), traceId: state.traceId, event: 'gate-block', skill: skillName, primary, allowed: Array.from(allowed), compliant: false, }); // 输出 deny 决策 const output = { hookSpecificOutput: { permissionDecision: 'deny', }, systemMessage: `[BWR 合规校验] ${skillName} 不在路由推荐集合中。推荐: ${primary}。请使用 /${primary} 或候选技能处理此请求。`, }; process.stderr.write(JSON.stringify(output)); process.exit(2); }).catch((e) => { // Fail-close: 安全组件解析异常 → 请求用户确认而非静默放行 try { logCompliance({ ts: new Date().toISOString(), traceId: 'error', event: 'gate-parse-error', error: (e && e.message) || 'unknown', }); } catch {} process.stderr.write(JSON.stringify({ hookSpecificOutput: { permissionDecision: 'ask' }, systemMessage: `[合规门控] 校验遇到异常(${(e && e.message) || 'unknown'}),请用户确认是否继续。`, })); process.exit(2); }); } // 模块导出 (供测试) if (typeof module !== 'undefined') { module.exports = { detectClaudeRoot: require('./lib/root.js'), loadRouteState, buildAllowedSet, logCompliance }; } if (require.main === module) { main(); }