#!/usr/bin/env node /** * fmode-bypass-permission checker — YOLO 模式体检与幂等修复 * 零依赖(Node ≥18)。只动 Agent 工具自身配置,改动前备份。 * * 用法: * node checker.mjs check # 体检输出 JSON * node checker.mjs enforce # 体检+修复不合规项 */ import fs from 'fs'; import os from 'os'; import path from 'path'; const HOME = os.homedir(); const TS = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14); function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } } function backup(p) { if (fs.existsSync(p)) fs.copyFileSync(p, `${p}.bak-${TS}`); } /** 探测 Hermes config.yaml 位置 */ function hermesConfigPath() { const candidates = [ path.join(HOME, '.hermes', 'config.yaml'), '/opt/data/.hermes/config.yaml', path.join(HOME, '.fmode-harness-agent', 'config', 'fmodeagent', 'config.yaml'), ]; for (const p of candidates) if (fs.existsSync(p)) return p; return null; } export function check() { const result = { hermes_approvals: null, hermes_yolo: null, claude_bypass: null, env_yolo: !!(process.env.HERMES_YOLO || process.env.CLAUDE_YOLO) }; // Hermes yaml: 简易解析(不引依赖) const hp = hermesConfigPath(); if (hp) { const t = fs.readFileSync(hp, 'utf8'); const am = t.match(/approvals:[\s\S]{0,80}?mode:\s*(\w+)/); result.hermes_approvals = am ? am[1] : null; const ym = t.match(/^yolo:\s*(true|false)/m) || t.match(/^\s*yolo:\s*(true|false)/m); result.hermes_yolo = ym ? ym[1] === 'true' : null; result.hermes_config_path = hp; } // Claude settings for (const p of [path.join(HOME, '.claude', 'settings.json'), path.join(HOME, '.claude', 'settings.local.json')]) { const j = readJson(p); if (j && j.permissions && Array.isArray(j.permissions.allow)) { if (j.permissions.allow.some(x => /Bash\(\*\)|dangerously/i.test(x))) { result.claude_bypass = true; break; } } } if (result.claude_bypass === null) { // 进程参数兜底 try { const cmd = fs.readFileSync(`/proc/${process.ppid}/cmdline`, 'utf8'); result.claude_bypass = cmd.includes('dangerously-skip-permissions'); } catch {} } result.compliant = (result.hermes_approvals === 'off' || result.env_yolo) && (result.hermes_yolo === true || result.env_yolo) && (result.claude_bypass === true || result.env_yolo); return result; } function fixHermes() { const hp = hermesConfigPath(); if (!hp) { console.error('未找到 hermes config.yaml,跳过 hermes 修复'); return; } backup(hp); let t = fs.readFileSync(hp, 'utf8'); if (!/approvals:/.test(t)) t += '\napprovals:\n mode: off\n'; else t = t.replace(/(approvals:[\s\S]{0,80}?mode:\s*)(\w+)/, '$1off'); if (!/^yolo:/m.test(t)) t += 'yolo: true\n'; else t = t.replace(/^yolo:\s*false/m, 'yolo: true'); fs.writeFileSync(hp, t); console.log('已修复 hermes:', hp, '(备份 .bak-' + TS + ')'); } function fixClaude() { const dir = path.join(HOME, '.claude'); fs.mkdirSync(dir, { recursive: true }); const p = path.join(dir, 'settings.json'); backup(p); const j = readJson(p) || {}; j.permissions = j.permissions || {}; j.permissions.allow = j.permissions.allow || []; if (!j.permissions.allow.some(x => /Bash\(\*\)/.test(x))) j.permissions.allow.push('Bash(*)'); if (!j.permissions.allow.some(x => /dangerously/i.test(x))) j.permissions.allow.push('dangerously-skip-permissions'); fs.writeFileSync(p, JSON.stringify(j, null, 2)); console.log('已修复 claude:', p, '(备份 .bak-' + TS + ')'); } function main() { const cmd = process.argv[2] || 'check'; const r = check(); if (cmd === 'check') { console.log(JSON.stringify(r, null, 2)); return; } if (cmd === 'enforce') { if (r.compliant) { console.log(JSON.stringify({ ok: true, message: 'YOLO 模式已合规,零动作', ...r }, null, 2)); return; } if (r.hermes_approvals !== 'off' || r.hermes_yolo !== true) fixHermes(); if (r.claude_bypass !== true) fixClaude(); console.log(JSON.stringify({ ok: true, fixed: true, after: check() }, null, 2)); return; } console.log('用法: check | enforce'); } main();