guanghulab/scripts/skyeye/dev-portal-guard.js
2026-05-10 13:12:44 +08:00

297 lines
11 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.

#!/usr/bin/env node
// scripts/skyeye/dev-portal-guard.js
// 天眼·开发者门户沙箱守卫 (Dev Portal Sandbox Guard)
//
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
//
// 目的:保护开发者门户的沙箱隔离
// 每位开发者只能部署到自己的频道 (channels/DEV-XXX/)
// 门户框架文件 (index.html, manifest.json) 受保护
// 必须提供完整身份三要素:开发者编号 + 姓名 + 人格体编号
// 匿名代理(无身份信息)= 禁止部署到任何真实路径
//
// 输入环境变量:
// PR_BRANCH — 分支名
// PR_TITLE — PR 标题
// PR_COMMITS — Commit 信息路径
// PR_FILES — 变更文件列表路径
// NOTION_API_KEY — Notion API 密钥(可选,用于跟踪)
// DEV_PROFILE_DB_ID — 开发者画像数据库 ID可选
//
// 输出:
// exit 0 → 允许部署
// exit 1 → 禁止部署
// GITHUB_OUTPUT → decision, dev_id, dev_name, persona_id, channel_files
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '../..');
const BRAIN_CONFIG_PATH = path.join(ROOT, '.github/persona-brain/gate-guard-config.json');
const MANIFEST_PATH = path.join(ROOT, 'docs/dev-portal/manifest.json');
const PR_FILES_PATH = process.env.PR_FILES || '/tmp/pr_files.txt';
const PR_COMMITS_PATH = process.env.PR_COMMITS || '/tmp/pr_commits.txt';
const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null';
// ━━━ 门户保护文件 — 开发者不可修改 ━━━
const PORTAL_FRAMEWORK_FILES = [
'docs/dev-portal/index.html',
'docs/dev-portal/manifest.json'
];
// ━━━ 安全读取 JSON ━━━
function readJSON(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
function setOutput(key, value) {
try {
fs.appendFileSync(GITHUB_OUTPUT, `${key}=${value}\n`);
} catch (e) { /* silent */ }
}
// ━━━ 从 PR 元数据中提取身份三要素 ━━━
function extractIdentity(prTitle, commitsPath) {
const identity = {
devId: null,
devName: null,
personaId: null,
complete: false,
source: []
};
// From PR title
if (prTitle) {
const devMatch = prTitle.match(/\b(DEV-\d{3})\b/i);
if (devMatch) { identity.devId = devMatch[1].toUpperCase(); identity.source.push('pr_title'); }
}
// From commit messages
try {
if (fs.existsSync(commitsPath)) {
const content = fs.readFileSync(commitsPath, 'utf8');
// DEV-XXX
if (!identity.devId) {
const devMatch = content.match(/\b(DEV-\d{3})\b/i);
if (devMatch) { identity.devId = devMatch[1].toUpperCase(); identity.source.push('commit_msg'); }
}
// Persona signature [PER-XXX]
const perMatch = content.match(/\[(PER-\d{3})\]/);
if (perMatch) { identity.personaId = perMatch[1]; identity.source.push('commit_sig'); }
// TCS-GL-XX format
const tcsMatch = content.match(/\b(TCS-GL-\d{2})\b/i);
if (tcsMatch && !identity.devId) { identity.devId = tcsMatch[1].toUpperCase(); identity.source.push('tcs_ref'); }
}
} catch (e) { /* non-fatal */ }
// From changed file paths — infer DEV-XXX from channel paths
try {
if (fs.existsSync(PR_FILES_PATH)) {
const files = fs.readFileSync(PR_FILES_PATH, 'utf8').split('\n').filter(Boolean);
for (const file of files) {
const channelMatch = file.match(/^docs\/dev-portal\/channels\/(DEV-\d{3})\//);
if (channelMatch) {
if (!identity.devId) {
identity.devId = channelMatch[1];
identity.source.push('file_path');
}
break;
}
}
}
} catch (e) { /* non-fatal */ }
// Cross-reference with gate-guard config for name and persona_id
if (identity.devId) {
const config = readJSON(BRAIN_CONFIG_PATH);
if (config && config.developer_permissions && config.developer_permissions[identity.devId]) {
const dev = config.developer_permissions[identity.devId];
identity.devName = dev.name;
if (!identity.personaId) identity.personaId = dev.persona_id;
}
}
// Completeness check: need all three (devId + devName + personaId)
identity.complete = !!(identity.devId && identity.devName && identity.personaId);
return identity;
}
// ━━━ 读取变更文件列表 ━━━
function loadPRFiles() {
try {
if (!fs.existsSync(PR_FILES_PATH)) return [];
return fs.readFileSync(PR_FILES_PATH, 'utf8')
.split('\n')
.map(f => f.trim())
.filter(f => f.length > 0);
} catch (e) {
return [];
}
}
// ━━━ 分类文件 ━━━
function classifyFiles(files) {
const result = {
portalFramework: [], // 门户框架文件(保护)
ownChannel: [], // 自己频道的文件
otherChannels: [], // 他人频道的文件
outsidePortal: [], // 门户以外的文件
channelDevIds: new Set() // 涉及的频道编号
};
for (const file of files) {
if (PORTAL_FRAMEWORK_FILES.includes(file)) {
result.portalFramework.push(file);
} else if (file.startsWith('docs/dev-portal/channels/')) {
const channelMatch = file.match(/^docs\/dev-portal\/channels\/(DEV-\d{3})\//);
if (channelMatch) {
result.channelDevIds.add(channelMatch[1]);
result.ownChannel.push(file); // Will be re-classified after identity check
}
} else if (file.startsWith('docs/dev-portal/')) {
result.portalFramework.push(file); // Any other dev-portal file is framework
} else {
result.outsidePortal.push(file);
}
}
return result;
}
// ━━━ 主逻辑 ━━━
function run() {
const branch = process.env.PR_BRANCH || '';
const prTitle = process.env.PR_TITLE || '';
console.log('═══════════════════════════════════════════════');
console.log('🛡️ 天眼 · 开发者门户沙箱守卫');
console.log('═══════════════════════════════════════════════');
console.log(`分支: ${branch}`);
console.log(`标题: ${prTitle}`);
console.log(`时间: ${new Date().toISOString()}`);
console.log('');
// ─── 提取身份三要素 ───
console.log('━━━ 身份验证 ━━━');
const identity = extractIdentity(prTitle, PR_COMMITS_PATH);
if (identity.devId) console.log(` 开发者编号: ${identity.devId}`);
if (identity.devName) console.log(` 开发者姓名: ${identity.devName}`);
if (identity.personaId) console.log(` 人格体编号: ${identity.personaId}`);
console.log(` 身份来源: ${identity.source.join(', ') || '无'}`);
console.log(` 三要素完整: ${identity.complete ? '✅ 是' : '❌ 否'}`);
console.log('');
// ─── 匿名屏蔽 ───
if (!identity.complete) {
console.log('❌ 身份验证失败 — 匿名代理禁止部署');
console.log('');
console.log(' 缺失信息:');
if (!identity.devId) console.log(' · 开发者编号 (DEV-XXX)');
if (!identity.devName) console.log(' · 开发者姓名');
if (!identity.personaId) console.log(' · 人格体编号 (PER-XXX)');
console.log('');
console.log(' 提示: 在 PR 标题或 commit 信息中包含 DEV-XXX 和 [PER-XXX]');
console.log(' 例: "DEV-002: deploy login module" 或 "[PER-002] feat: add feature"');
setOutput('decision', 'block');
setOutput('block_reason', 'anonymous_agent');
process.exit(1);
}
// ─── 分类变更文件 ───
const files = loadPRFiles();
if (files.length === 0) {
console.log('⚠️ 无变更文件 — 放行');
setOutput('decision', 'pass');
setOutput('dev_id', identity.devId);
process.exit(0);
}
const classified = classifyFiles(files);
const portalFiles = files.filter(f => f.startsWith('docs/dev-portal/'));
if (portalFiles.length === 0) {
console.log(' 此 PR 不涉及门户文件 — 跳过门户守卫');
setOutput('decision', 'skip');
setOutput('dev_id', identity.devId);
process.exit(0);
}
console.log('━━━ 文件分类 ━━━');
console.log(` 门户框架文件: ${classified.portalFramework.length}`);
classified.portalFramework.forEach(f => console.log(` 🔒 ${f}`));
console.log(` 频道文件: ${classified.ownChannel.length}`);
console.log(` 涉及频道: ${[...classified.channelDevIds].join(', ') || '无'}`);
console.log(` 门户外文件: ${classified.outsidePortal.length}`);
console.log('');
// ─── 检查框架文件保护 ───
if (classified.portalFramework.length > 0) {
console.log('❌ 禁止修改门户框架文件:');
classified.portalFramework.forEach(f => console.log(` 🔒 ${f}`));
console.log('');
console.log(' 门户框架文件由天眼系统维护,开发者不可修改');
setOutput('decision', 'block');
setOutput('block_reason', 'framework_tampering');
setOutput('dev_id', identity.devId);
process.exit(1);
}
// ─── 检查频道隔离 ───
const channelIds = [...classified.channelDevIds];
const touchesOtherChannels = channelIds.some(id => id !== identity.devId);
if (touchesOtherChannels) {
const otherChannels = channelIds.filter(id => id !== identity.devId);
console.log('❌ 频道隔离违规 — 修改了他人频道:');
otherChannels.forEach(id => console.log(` 🚫 ${id} (不属于 ${identity.devId})`));
console.log('');
console.log(' 每位开发者只能部署到自己的频道沙箱');
setOutput('decision', 'block');
setOutput('block_reason', 'channel_isolation');
setOutput('dev_id', identity.devId);
process.exit(1);
}
// ─── 验证频道归属 ───
if (channelIds.length === 1 && channelIds[0] === identity.devId) {
const channelFiles = classified.ownChannel;
console.log(`✅ 频道验证通过 — ${identity.devName}(${identity.devId}) → channels/${identity.devId}/`);
console.log(` 部署文件: ${channelFiles.length}`);
channelFiles.forEach(f => console.log(` 📦 ${f}`));
console.log('');
setOutput('decision', 'pass');
setOutput('dev_id', identity.devId);
setOutput('dev_name', identity.devName);
setOutput('persona_id', identity.personaId);
setOutput('channel_files', channelFiles.join(','));
console.log('═══ 决策: PASS ═══');
console.log(` ${identity.devName} 的模块可以部署到 channels/${identity.devId}/`);
process.exit(0);
}
// Default pass for edge cases
console.log('✅ 门户守卫检查通过');
setOutput('decision', 'pass');
setOutput('dev_id', identity.devId);
process.exit(0);
}
run();