guanghulab/scripts/gate-guard-v2.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

371 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

// 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();