// scripts/gate-guard-v2.js // 铸渊·智能门禁 v2 · 判定引擎 // // v2 升级内容(人格体编号体系): // ① 检查 commit 签名(PER-XXX 人格体编号) // ② 实时查询 Notion 天眼人格体注册表(降级回退到本地配置) // ③ 保留 v1 全部路径权限判定逻辑 // // 核心原则(冰朔确认): // 仓库主人(repo_owner)的推送 → 永远放行 // 系统 bot push → 永远放行 // 其他推送 → 检查签名 + 身份 + 路径权限 // // 输入:PUSH_ACTOR + /tmp/changed_files.txt + COMMIT_MESSAGE // 输出:action (pass/fix/revert) → GITHUB_OUTPUT 'use strict'; const fs = require('fs'); const path = require('path'); // ━━━ 配置路径 ━━━ const BRAIN_CONFIG_PATH = path.join(__dirname, '../.github/persona-brain/gate-guard-config.json'); const OWNER_CONFIG_PATH = path.join(__dirname, '../.github/gate-guard-config.json'); const CHANGED_FILES_PATH = '/tmp/changed_files.txt'; const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null'; // ━━━ 仓库主人 ━━━ const REPO_OWNER = 'qinfendebingshuo'; // ━━━ 人格体签名正则(v2 升级:支持 PER-XXX / TCS-XXX / PER-PENDING-XXX) ━━━ const PERSONA_SIGNATURE_REGEX = /^\[([A-Z]+-[A-Z0-9\-∞]+)\]/; // ━━━ 显示长度限制 ━━━ const MAX_COMMIT_DISPLAY = 80; // ━━━ 安全读取 JSON ━━━ function readJSON(filePath) { try { if (!fs.existsSync(filePath)) return null; return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (e) { console.error(`⚠️ 无法读取 ${path.basename(filePath)}: ${e.message}`); return null; } } // ━━━ 加载合并配置 ━━━ function loadConfig() { const brainConfig = readJSON(BRAIN_CONFIG_PATH); const ownerConfig = readJSON(OWNER_CONFIG_PATH); if (!brainConfig && !ownerConfig) { console.error('⚠️ 两套门禁配置均缺失'); return null; } const config = brainConfig || { whitelist_actors: [], system_protected_paths: [], developer_permissions: {} }; if (!config.whitelist_actors) config.whitelist_actors = []; const repoOwner = config.repo_owner || REPO_OWNER; if (!config.whitelist_actors.includes(repoOwner)) { config.whitelist_actors.push(repoOwner); } config.repo_owner = repoOwner; if (ownerConfig) { const ownerWhitelist = ownerConfig.whitelist || []; for (const user of ownerWhitelist) { if (!config.whitelist_actors.includes(user)) { config.whitelist_actors.push(user); } } if (ownerConfig.developers) { config._owner_developers = ownerConfig.developers; } } return config; } // ━━━ 读取变更文件列表 ━━━ function loadChangedFiles() { try { const content = fs.readFileSync(CHANGED_FILES_PATH, 'utf8').trim(); if (!content) return []; return content.split('\n').map(f => f.trim()).filter(f => f !== ''); } catch (e) { console.error('⚠️ 无法读取变更文件列表:', e.message); return []; } } // ━━━ 提取 commit 签名中的人格体编号 ━━━ function extractPersonaSignature(commitMessage) { if (!commitMessage) return null; const match = commitMessage.match(PERSONA_SIGNATURE_REGEX); if (match) { return match[1]; // e.g. "PER-SS001", "TCS-0002∞", "PER-PENDING-005" } return null; } // ━━━ 通过人格体编号查找开发者 ━━━ function findByPersonaId(config, personaId) { if (!config || !config.developer_permissions) return null; for (const [devId, dev] of Object.entries(config.developer_permissions)) { if (dev.persona_id === personaId) { return { devId, ...dev }; } } return null; } // ━━━ 查找开发者权限(双配置源 + 人格体编号查找) ━━━ function findDeveloper(config, actor, personaId) { if (!config) return null; // 1. 优先通过 persona_id 查找(v2 新增) if (personaId) { const byPersona = findByPersonaId(config, personaId); if (byPersona) { console.log(`🆔 通过人格体编号 ${personaId} 识别: ${byPersona.name}`); return byPersona; } } // 2. 从 persona-brain 配置(DEV-ID 索引)中按 github_usernames 查找 if (config.developer_permissions) { for (const [devId, dev] of Object.entries(config.developer_permissions)) { if (dev.github_usernames && dev.github_usernames.includes(actor)) { return { devId, ...dev }; } } } // 3. 从冰朔确认版配置中查找 if (config._owner_developers && config._owner_developers[actor]) { const dev = config._owner_developers[actor]; return { devId: dev.dev_id, name: dev.name, allowed_paths: dev.allowed_paths || [], github_usernames: [actor] }; } return null; } // ━━━ 检查文件是否在签名豁免路径(如 syslog-inbox/) ━━━ function isExemptPath(filePath, exemptPaths) { if (!exemptPaths || exemptPaths.length === 0) return false; return exemptPaths.some(p => { if (p.endsWith('/')) { return filePath.startsWith(p); } return filePath === p; }); } // ━━━ 检查文件是否在系统保护路径 ━━━ function isProtectedPath(filePath, protectedPaths) { return protectedPaths.some(p => { if (p.endsWith('/')) { return filePath.startsWith(p); } return filePath === p; }); } // ━━━ 检查文件是否在允许路径 ━━━ function isAllowedPath(filePath, allowedPaths) { if (!allowedPaths || allowedPaths.length === 0) return false; return allowedPaths.some(p => { if (p.endsWith('/')) { return filePath.startsWith(p); } return filePath === p; }); } // ━━━ 输出到 GITHUB_OUTPUT ━━━ function setOutput(key, value) { try { fs.appendFileSync(GITHUB_OUTPUT, `${key}=${value}\n`); } catch (e) { console.log(`::set-output name=${key}::${value}`); } } // ━━━ 主判定逻辑 ━━━ function main() { const actor = process.env.PUSH_ACTOR || ''; const commitMessage = process.env.COMMIT_MESSAGE || ''; console.log(`🚦 铸渊·智能门禁 v2 · 判定引擎启动`); console.log(` 推送者: ${actor}`); console.log(` Commit: ${commitMessage.substring(0, MAX_COMMIT_DISPLAY)}`); // 0. 仓库主人 → 永远放行 if (actor === REPO_OWNER) { console.log(`👑 ${actor} 是仓库主人,直接放行`); setOutput('action', 'pass'); setOutput('notification', `仓库主人 ${actor} 放行`); return; } // 1. 读取配置 const config = loadConfig(); if (!config) { console.log('⚠️ 配置文件缺失,默认放行'); setOutput('action', 'pass'); setOutput('notification', '门禁配置缺失,默认放行'); return; } // 2. 读取变更文件 const changedFiles = loadChangedFiles(); if (changedFiles.length === 0) { console.log('ℹ️ 无变更文件,放行'); setOutput('action', 'pass'); setOutput('notification', '无变更文件'); return; } console.log(` 变更文件 (${changedFiles.length}):`); changedFiles.forEach(f => console.log(` - ${f}`)); // 3. 白名单检查 if (config.whitelist_actors && config.whitelist_actors.includes(actor)) { console.log(`✅ ${actor} 在白名单中,直接放行`); setOutput('action', 'pass'); setOutput('notification', `白名单用户 ${actor} 放行`); return; } // 3.5 【v2 修复】签名豁免路径检查(如 syslog-inbox/) const exemptPaths = config.signature_exempt_paths || []; if (exemptPaths.length > 0 && changedFiles.length > 0) { const allExempt = changedFiles.every(f => isExemptPath(f, exemptPaths)); if (allExempt) { console.log(`📋 所有变更文件均在签名豁免路径内,跳过签名和路径校验,直接放行`); exemptPaths.forEach(p => console.log(` 豁免路径: ${p}`)); setOutput('action', 'pass'); setOutput('notification', `${actor} 的 push 全部在签名豁免路径内,放行`); return; } } // 4. 【v2 新增】提取 commit 签名中的人格体编号 const personaId = extractPersonaSignature(commitMessage); if (personaId) { console.log(`🔏 检测到人格体签名: ${personaId}`); } else { console.log(`ℹ️ 未检测到人格体签名(兼容 v1 模式)`); } // 5. 查找开发者身份(v2: 支持人格体编号 + GitHub 用户名双重匹配) const developer = findDeveloper(config, actor, personaId); const protectedPaths = config.system_protected_paths || []; // 6. 签名与身份交叉验证(v2 增强安全) if (personaId && developer) { if (developer.persona_id && developer.persona_id !== personaId) { console.log(`⚠️ 签名人格体 ${personaId} 与 actor ${actor} 注册的 ${developer.persona_id} 不匹配`); const msg = `⚠️ 人格体签名不匹配 · 推送者: ${actor}\n\n` + `Commit 签名的人格体: ${personaId}\n` + `GitHub Actor 注册的人格体: ${developer.persona_id}\n\n` + `签名与身份不一致,该 commit 已被回退。`; setOutput('action', 'revert'); setOutput('notification', msg.replace(/\n/g, '%0A')); setOutput('violation_type', 'persona_mismatch'); return; } } // 7. 分类文件(与 v1 逻辑一致) const protectedViolations = []; const allowedFiles = []; const disallowedFiles = []; for (const file of changedFiles) { if (isProtectedPath(file, protectedPaths)) { protectedViolations.push(file); } else if (developer && isAllowedPath(file, developer.allowed_paths)) { allowedFiles.push(file); } else if (developer) { disallowedFiles.push(file); } else { disallowedFiles.push(file); } } console.log(`\n📊 判定结果:`); console.log(` 系统保护路径违规: ${protectedViolations.length}`); console.log(` 允许路径文件: ${allowedFiles.length}`); console.log(` 越权路径文件: ${disallowedFiles.length}`); if (personaId) console.log(` 人格体签名: ${personaId} ✅`); // 8. 判定行动(与 v1 逻辑一致) if (protectedViolations.length > 0) { const msg = `⛔ 系统保护路径违规 · 推送者: ${actor}${personaId ? ` [${personaId}]` : ''}\n\n` + `以下文件属于系统保护路径,合作者不可修改:\n` + protectedViolations.map(f => `- \`${f}\``).join('\n') + `\n\n该 commit 已被自动回退。请联系管理员。`; console.log('❌ 判定: revert(系统保护路径违规)'); setOutput('action', 'revert'); setOutput('notification', msg.replace(/\n/g, '%0A')); setOutput('violation_type', 'system_protected'); setOutput('violation_files', protectedViolations.join(',')); return; } if (disallowedFiles.length > 0 && allowedFiles.length === 0) { const devInfo = developer ? `已注册开发者 ${developer.name}(${developer.devId})` : `未注册开发者`; const allowedInfo = developer && developer.allowed_paths.length > 0 ? `允许路径: ${developer.allowed_paths.join(', ')}` : '无已分配路径'; const msg = `⚠️ 路径越权 · 推送者: ${actor}${personaId ? ` [${personaId}]` : ''} · ${devInfo}\n\n` + `${allowedInfo}\n\n` + `以下文件不在你的允许路径内:\n` + disallowedFiles.map(f => `- \`${f}\``).join('\n') + `\n\n该 commit 已被自动回退。请确认你的模块路径。`; console.log('❌ 判定: revert(全部越权)'); setOutput('action', 'revert'); setOutput('notification', msg.replace(/\n/g, '%0A')); setOutput('violation_type', 'path_unauthorized'); setOutput('violation_files', disallowedFiles.join(',')); return; } if (disallowedFiles.length > 0 && allowedFiles.length > 0) { const devName = developer ? `${developer.name}(${developer.devId})` : `未注册开发者`; const devPaths = developer && developer.allowed_paths ? developer.allowed_paths.join(', ') : '无'; const msg = `⚠️ 部分路径越权 · 推送者: ${actor}${personaId ? ` [${personaId}]` : ''} · ${devName}\n\n` + `允许路径: ${devPaths}\n\n` + `✅ 合法文件:\n` + allowedFiles.map(f => `- \`${f}\``).join('\n') + '\n\n' + `❌ 越权文件:\n` + disallowedFiles.map(f => `- \`${f}\``).join('\n') + `\n\n该 commit 已被自动回退。请将越权文件移除后重新提交。`; console.log('❌ 判定: revert(部分越权)'); setOutput('action', 'revert'); setOutput('notification', msg.replace(/\n/g, '%0A')); setOutput('violation_type', 'partial_unauthorized'); setOutput('violation_files', disallowedFiles.join(',')); return; } // 全部文件在允许路径内 → 放行 if (developer) { const personaInfo = personaId ? ` · 签名:${personaId}` : ''; console.log(`✅ 判定: pass · ${developer.name}(${developer.devId})${personaInfo} · 全部文件合法`); } else { console.log(`✅ 判定: pass · 未注册开发者 ${actor} · 未修改保护路径`); } setOutput('action', 'pass'); setOutput('notification', `${actor}${personaId ? ` [${personaId}]` : ''} 的 push 已通过门禁检查`); if (personaId) setOutput('persona_id', personaId); } main();