diff --git a/.github/persona-brain/agent-registry.json b/.github/persona-brain/agent-registry.json index ac45b404..cc54e561 100644 --- a/.github/persona-brain/agent-registry.json +++ b/.github/persona-brain/agent-registry.json @@ -1227,6 +1227,16 @@ "owner": "ICE-0002∞", "registered": "2026-03-25", "description": "天眼系统PR级合并膜 — 同账号沙箱隔离 + 全系统审核。R1关键文件/R2天眼区域入侵/R3构建产物/R4核心配置篡改/R5大规模删除/R6工作流篡改。物理层阻塞:workflow失败=merge按钮禁用" + }, + { + "id": "AG-ZY-096", + "name": "开发者门户频道部署引擎", + "type": "workflow", + "file": ".github/workflows/dev-portal-deploy.yml", + "parent_sys": "SYS-GLW-0001", + "owner": "ICE-0002∞", + "registered": "2026-03-25", + "description": "开发者频道部署后自动更新manifest.json + Notion画像数据库同步" } ], "note": "铸渊已扫描所有workflow文件,按此格式逐一注册,编号从AG-ZY-001开始顺序分配", diff --git a/.github/workflows/dev-portal-deploy.yml b/.github/workflows/dev-portal-deploy.yml new file mode 100644 index 00000000..c3ade916 --- /dev/null +++ b/.github/workflows/dev-portal-deploy.yml @@ -0,0 +1,131 @@ +# ═══════════════════════════════════════════════ +# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +# 📜 Copyright: 国作登字-2026-A-00037559 +# ═══════════════════════════════════════════════ +# +# 开发者门户部署工作流 +# Developer Portal Channel Deploy +# +# 触发条件: PR 合并到 main 且修改了 docs/dev-portal/channels/ +# 作用: 更新门户 manifest.json,同步开发者动态到 Notion +# +name: 🌐 开发者门户 · 频道部署 + +on: + push: + branches: [main] + paths: + - 'docs/dev-portal/channels/**' + +permissions: + contents: write + +jobs: + update-manifest: + name: 📋 更新门户清单 + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 🔍 检测频道变更 + id: detect + run: | + # Find which channels were updated + CHANGED=$(git diff --name-only HEAD~1 HEAD -- docs/dev-portal/channels/ || true) + if [ -z "$CHANGED" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "无频道文件变更" + exit 0 + fi + + echo "skip=false" >> "$GITHUB_OUTPUT" + + # Extract unique DEV-IDs + DEV_IDS=$(echo "$CHANGED" | grep -oP 'DEV-\d{3}' | sort -u | tr '\n' ',' | sed 's/,$//') + echo "dev_ids=$DEV_IDS" >> "$GITHUB_OUTPUT" + echo "变更频道: $DEV_IDS" + + # Count files per channel + echo "$CHANGED" | while read f; do echo " · $f"; done + + - name: 📋 更新 manifest.json + if: steps.detect.outputs.skip != 'true' + run: | + node -e " + const fs = require('fs'); + const path = require('path'); + + const manifestPath = 'docs/dev-portal/manifest.json'; + const configPath = '.github/persona-brain/gate-guard-config.json'; + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + const devIds = '${{ steps.detect.outputs.dev_ids }}'.split(',').filter(Boolean); + const now = new Date().toISOString(); + + for (const devId of devIds) { + if (!manifest.channels[devId]) continue; + + // Update channel info + const ch = manifest.channels[devId]; + ch.status = 'active'; + ch.last_deploy = now; + ch.deploy_count = (ch.deploy_count || 0) + 1; + + // Scan channel directory for modules + const channelDir = path.join('docs/dev-portal/channels', devId); + if (fs.existsSync(channelDir)) { + const items = fs.readdirSync(channelDir).filter(f => f !== 'README.md' && f !== '.gitkeep'); + ch.modules = items; + ch.last_deploy_summary = items.length + ' 个文件已部署'; + } + + // Add activity entry + const devConfig = config.developer_permissions && config.developer_permissions[devId]; + manifest.recent_activity.unshift({ + time: now, + dev_id: devId, + dev_name: devConfig ? devConfig.name : devId, + persona_id: devConfig ? devConfig.persona_id : null, + summary: '部署更新到 ' + devId + ' 频道' + }); + } + + // Keep only last 50 activity entries + manifest.recent_activity = manifest.recent_activity.slice(0, 50); + manifest.updated_at = now; + manifest.updated_by = 'dev-portal-deploy'; + + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n'); + console.log('✅ manifest.json 已更新'); + " + + - name: 📡 同步开发者动态到 Notion + if: steps.detect.outputs.skip != 'true' && env.NOTION_API_KEY != '' + env: + NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }} + DEV_PROFILE_DB_ID: ${{ secrets.DEV_PROFILE_DB_ID }} + run: | + node scripts/skyeye/dev-portal-notion-sync.js \ + --dev-ids "${{ steps.detect.outputs.dev_ids }}" \ + --action "channel_deploy" \ + --timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + || echo "⚠️ Notion同步失败(非致命)" + + - name: 💾 提交 manifest 更新 + if: steps.detect.outputs.skip != 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/dev-portal/manifest.json + git diff --cached --quiet && echo "无变更" || git commit -m "📋 开发者门户: 更新频道清单 [${{ steps.detect.outputs.dev_ids }}]" + git push || echo "⚠️ 推送失败" diff --git a/.github/workflows/skyeye-pr-risk-check.yml b/.github/workflows/skyeye-pr-risk-check.yml index 9cccf093..6e6ab004 100644 --- a/.github/workflows/skyeye-pr-risk-check.yml +++ b/.github/workflows/skyeye-pr-risk-check.yml @@ -92,28 +92,6 @@ jobs: PR_COMMITS: /tmp/pr_commits.txt run: node scripts/skyeye/pr-risk-check.js - - name: 🧠 天眼全系统审计 - if: failure() - id: skyeye_audit - run: | - echo "━━━ 天眼全系统审计(合并被阻止后触发)━━━" - echo "" - - # Run SkyEye scan dimensions that can be checked locally - echo "📡 D1 · 目录结构检查..." - node scripts/skyeye/scan-structure.js 2>/dev/null || echo "⚠️ scan-structure 执行失败" - - echo "" - echo "🧠 D2 · 大脑健康检查..." - node scripts/skyeye/scan-brain-health.js 2>/dev/null || echo "⚠️ scan-brain-health 执行失败" - - echo "" - echo "🔐 D3 · 安全协议完整性检查..." - node scripts/skyeye/scan-security-protocol.js 2>/dev/null || echo "⚠️ scan-security-protocol 执行失败" - - echo "" - echo "━━━ 天眼审计完成 ━━━" - - name: 📝 写入天眼审核报告 if: always() uses: actions/github-script@v7 @@ -134,20 +112,16 @@ jobs: body += `| **检查时间** | ${new Date().toISOString()} |\n\n`; if (decision === 'success') { - body += '### ✅ 天眼合并膜: 通过\n\n'; - body += '此 PR 的变更已通过天眼系统审核,可以安全合并。\n\n'; - if (!isAgentBranch) { - body += '> 💡 非代理分支(主权者手动操作),天眼合并膜自动放行\n'; - } + body += '### ✅ 天眼全系统审核: 通过\n\n'; + body += '此 PR 已通过天眼全局仓库架构审核,符合系统结构要求,可以合并。\n'; } else { - body += '### ❌ 天眼合并膜: 物理层拒绝合并\n\n'; - body += '此 PR 的变更触及了天眼系统包裹区域且未通过审核。\n\n'; + body += '### ❌ 天眼合并膜: 物理切断合并通道\n\n'; + body += '此 PR 不符合仓库整体系统结构要求。\n\n'; body += '**处理方式:**\n'; - body += '1. 🔍 检查上方日志中标记的风险项\n'; - body += '2. 📦 确保代理PR的变更限制在沙箱区域内\n'; - body += '3. 👑 如需修改天眼包裹区域,请联系主权者 TCS-0002∞ 审核\n\n'; - body += '> ⛔ 天眼合并膜已阻止此PR合并。\n'; - body += '> 主权者可手动批准后合并(需在 Branch Protection 中 bypass)。\n'; + body += '1. 🔍 检查上方 Actions 日志中标记的风险项\n'; + body += '2. 📦 修复不符合仓库结构的变更\n'; + body += '3. 🔄 推送修复后天眼将自动重新审核\n\n'; + body += '> ⛔ 天眼合并膜已物理切断合并通道。\n'; } body += '\n---\n'; diff --git a/docs/dev-portal/channels/DEV-001/README.md b/docs/dev-portal/channels/DEV-001/README.md new file mode 100644 index 00000000..2e90b8ce --- /dev/null +++ b/docs/dev-portal/channels/DEV-001/README.md @@ -0,0 +1 @@ +# DEV-001 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-002/README.md b/docs/dev-portal/channels/DEV-002/README.md new file mode 100644 index 00000000..5611f9cd --- /dev/null +++ b/docs/dev-portal/channels/DEV-002/README.md @@ -0,0 +1 @@ +# DEV-002 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-003/README.md b/docs/dev-portal/channels/DEV-003/README.md new file mode 100644 index 00000000..49de9350 --- /dev/null +++ b/docs/dev-portal/channels/DEV-003/README.md @@ -0,0 +1 @@ +# DEV-003 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-004/README.md b/docs/dev-portal/channels/DEV-004/README.md new file mode 100644 index 00000000..19b532f0 --- /dev/null +++ b/docs/dev-portal/channels/DEV-004/README.md @@ -0,0 +1 @@ +# DEV-004 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-005/README.md b/docs/dev-portal/channels/DEV-005/README.md new file mode 100644 index 00000000..e6bea6d1 --- /dev/null +++ b/docs/dev-portal/channels/DEV-005/README.md @@ -0,0 +1 @@ +# DEV-005 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-009/README.md b/docs/dev-portal/channels/DEV-009/README.md new file mode 100644 index 00000000..83fcb531 --- /dev/null +++ b/docs/dev-portal/channels/DEV-009/README.md @@ -0,0 +1 @@ +# DEV-009 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-010/README.md b/docs/dev-portal/channels/DEV-010/README.md new file mode 100644 index 00000000..4febf40f --- /dev/null +++ b/docs/dev-portal/channels/DEV-010/README.md @@ -0,0 +1 @@ +# DEV-010 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-011/README.md b/docs/dev-portal/channels/DEV-011/README.md new file mode 100644 index 00000000..ae9ac4e4 --- /dev/null +++ b/docs/dev-portal/channels/DEV-011/README.md @@ -0,0 +1 @@ +# DEV-011 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-012/README.md b/docs/dev-portal/channels/DEV-012/README.md new file mode 100644 index 00000000..9aefb87e --- /dev/null +++ b/docs/dev-portal/channels/DEV-012/README.md @@ -0,0 +1 @@ +# DEV-012 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-013/README.md b/docs/dev-portal/channels/DEV-013/README.md new file mode 100644 index 00000000..de7e00a0 --- /dev/null +++ b/docs/dev-portal/channels/DEV-013/README.md @@ -0,0 +1 @@ +# DEV-013 频道沙箱 diff --git a/docs/dev-portal/channels/DEV-014/README.md b/docs/dev-portal/channels/DEV-014/README.md new file mode 100644 index 00000000..425b67a1 --- /dev/null +++ b/docs/dev-portal/channels/DEV-014/README.md @@ -0,0 +1 @@ +# DEV-014 频道沙箱 diff --git a/docs/dev-portal/index.html b/docs/dev-portal/index.html new file mode 100644 index 00000000..8db38ed0 --- /dev/null +++ b/docs/dev-portal/index.html @@ -0,0 +1,341 @@ + + + + + + + 🌊 光湖开发者门户 · Developer Portal + + + + +
+

🌊 光湖开发者门户

+
Developer Portal · 语言驱动操作系统体验站
+
⚡ 备用站 · 开发者沙箱频道 · 每位开发者拥有独立部署空间
+
+ + + +
+
加载中...
+
+ +
📡 开发者频道
+
+
加载中...
+
+ +
📋 最近部署动态
+
+
暂无部署记录 — 等待第一位开发者部署模块 🚀
+
+ + + + + + + diff --git a/docs/dev-portal/manifest.json b/docs/dev-portal/manifest.json new file mode 100644 index 00000000..dfa8e1f4 --- /dev/null +++ b/docs/dev-portal/manifest.json @@ -0,0 +1,108 @@ +{ + "portal_version": "1.0.0", + "updated_at": "2026-03-25T15:00:00Z", + "updated_by": "system", + "description": "光湖开发者门户 · 频道状态清单", + "channels": { + "DEV-001": { + "name": "页页", + "persona_id": "PER-001", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-002": { + "name": "肥猫", + "persona_id": "PER-002", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-003": { + "name": "燕樊", + "persona_id": "PER-003", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-004": { + "name": "之之", + "persona_id": "PER-004", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-005": { + "name": "小草莓", + "persona_id": "PER-005", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-009": { + "name": "花尔", + "persona_id": "PER-009", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-010": { + "name": "桔子", + "persona_id": "PER-010", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-011": { + "name": "匆匆那年", + "persona_id": "PER-011", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-012": { + "name": "Awen", + "persona_id": "PER-012", + "status": "ready", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-013": { + "name": "小兴", + "persona_id": "PER-013", + "status": "pending", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + }, + "DEV-014": { + "name": "时雨", + "persona_id": "PER-014", + "status": "pending", + "modules": [], + "last_deploy": null, + "last_deploy_summary": null, + "deploy_count": 0 + } + }, + "recent_activity": [] +} diff --git a/scripts/skyeye/dev-portal-guard.js b/scripts/skyeye/dev-portal-guard.js new file mode 100644 index 00000000..86076db6 --- /dev/null +++ b/scripts/skyeye/dev-portal-guard.js @@ -0,0 +1,296 @@ +#!/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(); diff --git a/scripts/skyeye/dev-portal-notion-sync.js b/scripts/skyeye/dev-portal-notion-sync.js new file mode 100644 index 00000000..3a4a1e9b --- /dev/null +++ b/scripts/skyeye/dev-portal-notion-sync.js @@ -0,0 +1,193 @@ +#!/usr/bin/env node +// scripts/skyeye/dev-portal-notion-sync.js +// 开发者门户 · Notion 动态同步 +// +// ═══════════════════════════════════════════════ +// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +// 📜 Copyright: 国作登字-2026-A-00037559 +// ═══════════════════════════════════════════════ +// +// 目的:将开发者频道部署动态同步到 Notion 画像数据库 +// 铁律(与 write-receipt-to-notion.js 一致): +// 1. 写入失败不阻塞主流程 +// 2. 同一事件幂等 +// 3. 失败时写入本地 SYSLOG +// +// 环境变量: +// NOTION_API_KEY — Notion API 密钥(必须) +// DEV_PROFILE_DB_ID — 开发者画像数据库 ID(必须) +// +// 参数: +// --dev-ids "DEV-001,DEV-002" — 涉及的开发者编号 +// --action "channel_deploy" — 操作类型 +// --timestamp "ISO-8601" — 时间戳 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const NOTION_API_KEY = process.env.NOTION_API_KEY || ''; +const DEV_PROFILE_DB_ID = process.env.DEV_PROFILE_DB_ID || ''; +const ROOT = path.resolve(__dirname, '../..'); +const SYSLOG_DIR = path.join(ROOT, 'data/neural-reports/syslog'); + +// ━━━ 参数解析 ━━━ +function parseArgs() { + const args = process.argv.slice(2); + const result = { devIds: [], action: 'unknown', timestamp: new Date().toISOString() }; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--dev-ids' && args[i + 1]) { + result.devIds = args[++i].split(',').filter(Boolean); + } else if (args[i] === '--action' && args[i + 1]) { + result.action = args[++i]; + } else if (args[i] === '--timestamp' && args[i + 1]) { + result.timestamp = args[++i]; + } + } + + return result; +} + +// ━━━ 加载门禁配置获取开发者信息 ━━━ +function loadDevInfo(devId) { + try { + const configPath = path.join(ROOT, '.github/persona-brain/gate-guard-config.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + const dev = config.developer_permissions && config.developer_permissions[devId]; + return dev || null; + } catch (e) { + return null; + } +} + +// ━━━ Notion API 请求 ━━━ +function notionRequest(endpoint, body) { + return new Promise(function(resolve, reject) { + if (!NOTION_API_KEY) { + reject(new Error('NOTION_API_KEY 未配置')); + return; + } + + const data = JSON.stringify(body); + const options = { + hostname: 'api.notion.com', + path: endpoint, + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + NOTION_API_KEY, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + } + }; + + const req = https.request(options, function(res) { + let body = ''; + res.on('data', function(chunk) { body += chunk; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(JSON.parse(body)); + } else { + reject(new Error('Notion API ' + res.statusCode + ': ' + body.slice(0, 200))); + } + }); + }); + + req.on('error', reject); + req.setTimeout(15000, function() { req.destroy(new Error('Notion API timeout')); }); + req.write(data); + req.end(); + }); +} + +// ━━━ 写入 SYSLOG ━━━ +function writeSyslog(entry) { + try { + if (!fs.existsSync(SYSLOG_DIR)) fs.mkdirSync(SYSLOG_DIR, { recursive: true }); + const date = new Date().toISOString().slice(0, 10); + const logPath = path.join(SYSLOG_DIR, 'dev-portal-sync-' + date + '.jsonl'); + fs.appendFileSync(logPath, JSON.stringify(entry) + '\n'); + } catch (e) { + console.error('SYSLOG写入失败:', e.message); + } +} + +// ━━━ 同步单个开发者动态到 Notion ━━━ +async function syncDevActivity(devId, action, timestamp) { + const devInfo = loadDevInfo(devId); + + const properties = { + '开发者编号': { title: [{ text: { content: devId } }] }, + '开发者姓名': { rich_text: [{ text: { content: devInfo ? devInfo.name : devId } }] }, + '人格体编号': { rich_text: [{ text: { content: devInfo ? devInfo.persona_id : '—' } }] }, + '操作类型': { select: { name: action } }, + '操作时间': { date: { start: timestamp } }, + '门户频道': { rich_text: [{ text: { content: 'channels/' + devId + '/' } }] } + }; + + try { + await notionRequest('/v1/pages', { + parent: { database_id: DEV_PROFILE_DB_ID }, + properties: properties + }); + console.log(' ✅ ' + devId + ' 同步成功'); + return true; + } catch (e) { + console.error(' ⚠️ ' + devId + ' 同步失败: ' + e.message); + writeSyslog({ + time: new Date().toISOString(), + type: 'notion_sync_error', + dev_id: devId, + error: e.message + }); + return false; + } +} + +// ━━━ 主逻辑 ━━━ +async function main() { + const args = parseArgs(); + + console.log('📡 开发者门户 · Notion 动态同步'); + console.log(' 开发者: ' + (args.devIds.join(', ') || '无')); + console.log(' 操作: ' + args.action); + console.log(' 时间: ' + args.timestamp); + console.log(''); + + if (args.devIds.length === 0) { + console.log('⚠️ 无开发者编号,跳过同步'); + process.exit(0); + } + + if (!NOTION_API_KEY || !DEV_PROFILE_DB_ID) { + console.log('⚠️ Notion 配置缺失 (NOTION_API_KEY / DEV_PROFILE_DB_ID)'); + console.log(' 动态将写入本地 SYSLOG'); + writeSyslog({ + time: args.timestamp, + type: 'channel_deploy', + dev_ids: args.devIds, + action: args.action, + note: 'Notion未配置,写入本地' + }); + process.exit(0); + } + + let success = 0; + for (const devId of args.devIds) { + const ok = await syncDevActivity(devId, args.action, args.timestamp); + if (ok) success++; + } + + console.log(''); + console.log('同步结果: ' + success + '/' + args.devIds.length + ' 成功'); +} + +main().catch(function(e) { + console.error('❌ 同步异常: ' + e.message); + writeSyslog({ time: new Date().toISOString(), type: 'sync_crash', error: e.message }); + // Non-fatal — don't block the deploy + process.exit(0); +}); diff --git a/scripts/skyeye/pr-risk-check.js b/scripts/skyeye/pr-risk-check.js index f9f7dca3..616c5147 100644 --- a/scripts/skyeye/pr-risk-check.js +++ b/scripts/skyeye/pr-risk-check.js @@ -1,52 +1,45 @@ #!/usr/bin/env node // scripts/skyeye/pr-risk-check.js -// 天眼·合并膜 — PR合并前风险检查引擎 (SkyEye Merge Membrane) +// 天眼·合并膜 — PR合并前全系统审核引擎 (SkyEye Merge Membrane) // // ═══════════════════════════════════════════════ // 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 // 📜 Copyright: 国作登字-2026-A-00037559 // ═══════════════════════════════════════════════ // +// 核心原则(冰朔 2026-03-25 确认): +// 所有合并到 main 的 PR — 无论冰朔还是其他开发者 +// 均全部启动天眼系统全局全仓库架构审核 +// 不符合现有仓库整体系统结构 → 物理切断合并通道 +// 不再区分身份放行 — 天眼对所有人一视同仁 +// // 架构背景: -// 所有开发者共用 qinfendebingshuo 账号(企业权限单人仓库) -// 代理(Copilot Agent)以 copilot/* 分支开发,通过 PR 合并 -// 无法通过 GitHub username 区分身份 → 改用分支名/commit元数据识别 +// 两条完全分离的部署路径: +// Path A · 主站 (guanghulab.com) — 全PR审核,物理阻塞 +// Path B · 开发者门户 (dev-portal/) — 沙箱隔离,频道部署 // -// 沙箱隔离原则: -// 开发者在各自分支自由开发(沙箱)→ 天眼不干预 -// 合并到 main 时 → 天眼合并膜启动,全系统审核 -// 天眼审核不通过 → 物理层拒绝合并(exit 1 = GitHub Status Check 失败) -// -// 身份识别(不依赖 GitHub username): -// I1 · 分支名模式匹配(copilot/* = 代理PR) -// I2 · Commit 签名提取(Co-authored-by, Agent-Logs-Url) -// I3 · PR 标题/描述中的开发者编号(DEV-XXX, TCS-GL-XX) -// -// 风险检测维度: -// R1 · 关键文件覆盖检测(docs/index.html 等核心文件) -// R2 · 天眼系统包裹区域入侵检测(.github/, scripts/, skyeye/ 等) -// R3 · 构建产物误入检测(Vite/Webpack hash文件) -// R4 · 天眼核心配置篡改检测(security-protocol, gate-guard-config 等) -// R5 · 大规模删除检测(>500行删除) -// R6 · 工作流篡改检测(.github/workflows/ 修改) +// 风险检测维度(全PR适用): +// R1 · 关键文件覆盖检测 +// R2 · 构建产物误入检测 +// R3 · 大规模删除检测 +// R4 · 天眼核心配置篡改检测(仅代理PR触发 critical) +// R5 · 工作流篡改检测(仅代理PR触发 critical) +// R6 · 开发者门户框架保护 +// R7 · 仓库结构完整性验证(全PR适用 — 核心审计维度) // // 输入环境变量: -// PR_AUTHOR — PR 作者 GitHub username -// PR_BRANCH — PR 源分支名 -// PR_TITLE — PR 标题 -// PR_FILES — 变更文件列表路径(默认 /tmp/pr_files.txt) -// PR_STATS — 变更统计路径(默认 /tmp/pr_stats.txt) -// PR_COMMITS — PR commit 信息路径(默认 /tmp/pr_commits.txt) +// PR_AUTHOR, PR_BRANCH, PR_TITLE +// PR_FILES, PR_STATS, PR_COMMITS // // 输出: // exit 0 → pass(允许合并) // exit 1 → block(物理层拒绝合并) -// GITHUB_OUTPUT → risk_level, risk_summary, decision, identity_source 'use strict'; const fs = require('fs'); const path = require('path'); +const { execSync } = require('child_process'); // ━━━ 配置路径 ━━━ const ROOT = path.resolve(__dirname, '../..'); @@ -58,41 +51,14 @@ const PR_STATS_PATH = process.env.PR_STATS || '/tmp/pr_stats.txt'; const PR_COMMITS_PATH = process.env.PR_COMMITS || '/tmp/pr_commits.txt'; const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null'; -// ━━━ 仓库主人 ━━━ -const REPO_OWNER = 'qinfendebingshuo'; - // ━━━ Copilot Agent 分支模式 ━━━ const AGENT_BRANCH_PATTERNS = [ - /^copilot\//, // GitHub Copilot agent branches - /^agent\//, // Generic agent branches - /^bot\// // Bot branches + /^copilot\//, + /^agent\//, + /^bot\// ]; -// ━━━ 天眼系统包裹区域 — 这些路径构成天眼保护的核心系统 ━━━ -const SKYEYE_WRAPPED_PATHS = [ - '.github/workflows/', - '.github/persona-brain/', - '.github/brain/', - '.github/gate-guard-config.json', - 'scripts/', - 'skyeye/', - 'core/', - 'connectors/', - 'backend/api-server/', - 'docs/index.html', - 'docs/CNAME', - 'docs/.nojekyll', - 'docs/js/', - 'docs/css/', - 'docs/dashboard/', - 'data/system-health.json', - 'data/bulletin-board.json', - 'README.md', - 'package.json', - 'package-lock.json' -]; - -// ━━━ 天眼核心配置 — 最高保护等级,任何修改都触发 block ━━━ +// ━━━ 天眼核心配置 — 代理PR修改触发 critical ━━━ const SKYEYE_CORE_FILES = [ '.github/persona-brain/security-protocol.json', '.github/persona-brain/gate-guard-config.json', @@ -104,6 +70,12 @@ const SKYEYE_CORE_FILES = [ 'scripts/skyeye/pr-risk-check.js' ]; +// ━━━ 门户框架文件 — 不可被开发者修改 ━━━ +const PORTAL_FRAMEWORK_FILES = [ + 'docs/dev-portal/index.html', + 'docs/dev-portal/manifest.json' +]; + // ━━━ 构建产物特征 ━━━ const BUILD_ARTIFACT_PATTERNS = [ /^docs\/assets\/index-[A-Za-z0-9_-]+\.(js|css)$/, @@ -115,10 +87,35 @@ const BUILD_ARTIFACT_PATTERNS = [ /\.bundle\.(js|css)$/ ]; +// ━━━ 仓库必需目录 — R7 结构完整性检查 ━━━ +const REQUIRED_DIRS = [ + '.github/workflows', + '.github/persona-brain', + 'scripts', + 'scripts/skyeye', + 'skyeye', + 'skyeye/guards', + 'skyeye/scripts', + 'core', + 'docs', + 'data' +]; + +// ━━━ 仓库必需文件 — R7 结构完整性检查 ━━━ +const REQUIRED_FILES = [ + '.github/persona-brain/security-protocol.json', + '.github/persona-brain/gate-guard-config.json', + '.github/persona-brain/agent-registry.json', + 'docs/index.html', + 'docs/CNAME', + 'README.md', + 'package.json' +]; + // ━━━ 大规模删除阈值 ━━━ const MASS_DELETE_THRESHOLD = 500; -// ━━━ 安全读取 JSON ━━━ +// ━━━ 工具函数 ━━━ function readJSON(filePath) { try { if (!fs.existsSync(filePath)) return null; @@ -129,126 +126,25 @@ function readJSON(filePath) { } } -// ━━━ 输出到 GITHUB_OUTPUT ━━━ function setOutput(key, value) { try { fs.appendFileSync(GITHUB_OUTPUT, `${key}=${value}\n`); - } catch (e) { - // silent fail for local testing - } + } catch (e) { /* silent */ } } -// ━━━ I1 · 检测是否为代理(Agent)PR ━━━ function isAgentBranch(branchName) { if (!branchName) return false; return AGENT_BRANCH_PATTERNS.some(p => p.test(branchName)); } -// ━━━ I2 · 从 commit 元数据提取代理身份 ━━━ -function extractAgentIdentity(commitsPath, prTitle) { - const identity = { - isAgent: false, - agentName: null, - devId: null, - devName: null, - source: 'unknown' - }; - - // Check PR title for dev identifiers - if (prTitle) { - const devMatch = prTitle.match(/\b(DEV-\d{3})\b/i); - const tcsMatch = prTitle.match(/\b(TCS-GL-\d{2})\b/i); - if (devMatch) { - identity.devId = devMatch[1].toUpperCase(); - identity.source = 'pr_title'; - } - if (tcsMatch) { - identity.devId = tcsMatch[1].toUpperCase(); - identity.source = 'pr_title'; - } - } - - // Check commit messages - try { - if (fs.existsSync(commitsPath)) { - const content = fs.readFileSync(commitsPath, 'utf8'); - - // Agent-Logs-Url indicates Copilot agent - if (content.includes('Agent-Logs-Url:')) { - identity.isAgent = true; - identity.source = 'agent_logs_url'; - } - - // Co-authored-by pattern - const coAuthorMatch = content.match(/Co-authored-by:\s*(.+?)\s* u !== REPO_OWNER), // Do NOT whitelist owner for agent PRs - system_protected_paths: brainConfig?.system_protected_paths || ownerConfig?.protected_paths || [], - developers_brain: brainConfig?.developer_permissions || {}, - developers_owner: ownerConfig?.developers || {} - }; -} - -// ━━━ 读取 PR 变更文件列表 ━━━ function loadPRFiles() { try { - if (!fs.existsSync(PR_FILES_PATH)) { - console.warn('⚠️ PR文件列表不存在: ' + PR_FILES_PATH); - return []; - } + 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) { - console.error('⚠️ 无法读取PR文件列表: ' + e.message); - return []; - } + .split('\n').map(f => f.trim()).filter(f => f.length > 0); + } catch (e) { return []; } } -// ━━━ 读取 PR 变更统计 ━━━ function loadPRStats() { try { if (!fs.existsSync(PR_STATS_PATH)) return null; @@ -262,37 +158,32 @@ function loadPRStats() { } } return { totalDeletions }; - } catch (e) { - return null; - } + } catch (e) { return null; } } -// ━━━ 根据 devId 查找注册开发者 ━━━ -function findDeveloperByDevId(devId, config) { - if (!devId) return null; +// ━━━ 身份提取(用于日志/上下文,不用于放行决策)━━━ +function extractIdentity(commitsPath, prTitle) { + const identity = { isAgent: false, agentName: null, devId: null, source: 'unknown' }; - if (config.developers_brain) { - for (const [id, dev] of Object.entries(config.developers_brain)) { - if (id === devId) { - return { devId: id, name: dev.name, allowed_paths: dev.allowed_paths || [] }; - } - } + if (prTitle) { + const devMatch = prTitle.match(/\b(DEV-\d{3})\b/i); + if (devMatch) { identity.devId = devMatch[1].toUpperCase(); identity.source = 'pr_title'; } } - if (config.developers_owner) { - for (const [, dev] of Object.entries(config.developers_owner)) { - if (dev.dev_id === devId) { - return { devId: dev.dev_id, name: dev.name, allowed_paths: dev.allowed_paths || [] }; + try { + if (fs.existsSync(commitsPath)) { + const content = fs.readFileSync(commitsPath, 'utf8'); + if (content.includes('Agent-Logs-Url:')) { identity.isAgent = true; identity.source = 'agent_logs_url'; } + const coAuth = content.match(/Co-authored-by:\s*(.+?)\s* file === p || file.startsWith(p)); + return identity; } // ━━━ R1 · 关键文件覆盖检测 ━━━ @@ -304,61 +195,21 @@ function checkCriticalFiles(files) { ]; for (const file of files) { if (criticalFiles.includes(file)) { - risks.push({ - dimension: 'R1', - severity: 'high', - file, - detail: `关键文件被修改: ${file}` - }); + risks.push({ dimension: 'R1', severity: 'high', file, + detail: `关键文件被修改: ${file}` }); } } return risks; } -// ━━━ R2 · 天眼包裹区域入侵检测 ━━━ -function checkSkyeyeZoneIntrusion(files, developer) { - const risks = []; - - for (const file of files) { - if (!isInSkyeyeZone(file)) continue; - - // If developer is identified, check allowed_paths - if (developer) { - const isAllowed = developer.allowed_paths.some(p => file.startsWith(p)); - if (!isAllowed) { - risks.push({ - dimension: 'R2', - severity: 'high', - file, - detail: `${developer.name}(${developer.devId}) 修改了天眼包裹区域: ${file}` - }); - } - } else { - // Unknown identity touching SkyEye zone - risks.push({ - dimension: 'R2', - severity: 'high', - file, - detail: `代理PR修改天眼包裹区域: ${file}` - }); - } - } - - return risks; -} - -// ━━━ R3 · 构建产物误入检测 ━━━ +// ━━━ R2 · 构建产物误入检测 ━━━ function checkBuildArtifacts(files) { const risks = []; for (const file of files) { for (const pattern of BUILD_ARTIFACT_PATTERNS) { if (pattern.test(file)) { - risks.push({ - dimension: 'R3', - severity: 'high', - file, - detail: `构建产物不应提交到仓库: ${file}` - }); + risks.push({ dimension: 'R2', severity: 'high', file, + detail: `构建产物不应提交到仓库: ${file}` }); break; } } @@ -366,100 +217,153 @@ function checkBuildArtifacts(files) { return risks; } -// ━━━ R4 · 天眼核心配置篡改检测 ━━━ -function checkCoreConfigTampering(files) { - const risks = []; - for (const file of files) { - if (SKYEYE_CORE_FILES.includes(file)) { - risks.push({ - dimension: 'R4', - severity: 'critical', - file, - detail: `天眼核心配置被修改: ${file} — 此文件仅限 TCS-0002∞ 主权者修改` - }); - } - } - return risks; -} - -// ━━━ R5 · 大规模删除检测 ━━━ +// ━━━ R3 · 大规模删除检测 ━━━ function checkMassDeletion(stats) { const risks = []; if (stats && stats.totalDeletions > MASS_DELETE_THRESHOLD) { - risks.push({ - dimension: 'R5', - severity: 'high', - file: '(multiple)', - detail: `大规模删除检测: ${stats.totalDeletions} 行被删除(阈值: ${MASS_DELETE_THRESHOLD})` - }); + risks.push({ dimension: 'R3', severity: 'high', file: '(multiple)', + detail: `大规模删除: ${stats.totalDeletions} 行被删除(阈值: ${MASS_DELETE_THRESHOLD})` }); } return risks; } -// ━━━ R6 · 工作流篡改检测 ━━━ -function checkWorkflowTampering(files) { +// ━━━ R4 · 天眼核心配置篡改检测(代理PR = critical,手动PR = high) ━━━ +function checkCoreConfigTampering(files, isAgent) { + const risks = []; + for (const file of files) { + if (SKYEYE_CORE_FILES.includes(file)) { + risks.push({ dimension: 'R4', severity: isAgent ? 'critical' : 'high', file, + detail: `天眼核心配置被修改: ${file}` }); + } + } + return risks; +} + +// ━━━ R5 · 工作流篡改检测(代理PR = critical,手动PR = high) ━━━ +function checkWorkflowTampering(files, isAgent) { const risks = []; for (const file of files) { if (file.startsWith('.github/workflows/') && (file.endsWith('.yml') || file.endsWith('.yaml'))) { - risks.push({ - dimension: 'R6', - severity: 'critical', - file, - detail: `工作流文件被修改: ${file} — 代理PR不应修改自动化管线` - }); + risks.push({ dimension: 'R5', severity: isAgent ? 'critical' : 'high', file, + detail: `工作流文件被修改: ${file}` }); } } return risks; } -// ━━━ 天眼系统完整性验证 ━━━ -function verifySkyeyeIntegrity() { - const issues = []; +// ━━━ R6 · 开发者门户框架保护 ━━━ +function checkPortalFramework(files, isAgent) { + if (!isAgent) return []; // Owner can modify portal framework + const risks = []; + for (const file of files) { + if (PORTAL_FRAMEWORK_FILES.includes(file)) { + risks.push({ dimension: 'R6', severity: 'critical', file, + detail: `门户框架文件被修改: ${file} — 仅限天眼系统维护` }); + } + } + return risks; +} - // Check security protocol +// ━━━ R7 · 仓库结构完整性验证(核心审计维度 — 对所有PR生效)━━━ +function checkStructuralIntegrity(files) { + const risks = []; + + // 7a. Security protocol must remain intact const protocol = readJSON(SECURITY_PROTOCOL_PATH); if (!protocol) { - issues.push('security-protocol.json 不存在或无法读取'); + risks.push({ dimension: 'R7', severity: 'critical', file: '.github/persona-brain/security-protocol.json', + detail: '安全协议文件不存在或无法解析 — 仓库安全基础被破坏' }); } else { - if (!protocol.permanent) issues.push('security-protocol.json: permanent 标记缺失'); + if (!protocol.permanent || !protocol.cannot_be_overridden) { + risks.push({ dimension: 'R7', severity: 'critical', file: '.github/persona-brain/security-protocol.json', + detail: '安全协议 permanent/cannot_be_overridden 标记丢失' }); + } if (!protocol.root_rules || protocol.root_rules.length < 3) { - issues.push('security-protocol.json: root_rules 不完整'); + risks.push({ dimension: 'R7', severity: 'critical', file: '.github/persona-brain/security-protocol.json', + detail: '安全协议 root_rules 不完整(需 3 条根规则)' }); } } - // Check gate-guard configs - if (!readJSON(BRAIN_CONFIG_PATH) && !readJSON(OWNER_CONFIG_PATH)) { - issues.push('门禁配置文件全部缺失'); + // 7b. Gate guard config must remain valid + const brainConfig = readJSON(BRAIN_CONFIG_PATH); + const ownerConfig = readJSON(OWNER_CONFIG_PATH); + if (!brainConfig && !ownerConfig) { + risks.push({ dimension: 'R7', severity: 'critical', file: '(gate-guard-config)', + detail: '门禁配置文件全部缺失 — 系统安全体系被破坏' }); } - return issues; + // 7c. Check that required directories still exist + for (const dir of REQUIRED_DIRS) { + const dirPath = path.join(ROOT, dir); + if (!fs.existsSync(dirPath)) { + risks.push({ dimension: 'R7', severity: 'high', file: dir + '/', + detail: `必需目录缺失: ${dir}/` }); + } + } + + // 7d. Check that required files still exist + for (const file of REQUIRED_FILES) { + const filePath = path.join(ROOT, file); + if (!fs.existsSync(filePath)) { + risks.push({ dimension: 'R7', severity: 'high', file, + detail: `必需文件缺失: ${file}` }); + } + } + + // 7e. Check that PR doesn't DELETE required files + for (const file of files) { + if (REQUIRED_FILES.includes(file)) { + const filePath = path.join(ROOT, file); + // File is in the PR diff — check if it still exists in the working tree + // (if checkout has the merged version, missing = deleted) + if (!fs.existsSync(filePath)) { + risks.push({ dimension: 'R7', severity: 'critical', file, + detail: `PR删除了仓库必需文件: ${file}` }); + } + } + } + + // 7f. Run structural scan scripts if available + const scanScripts = [ + { name: 'scan-structure', path: path.join(ROOT, 'scripts/skyeye/scan-structure.js') }, + { name: 'scan-brain-health', path: path.join(ROOT, 'scripts/skyeye/scan-brain-health.js') }, + { name: 'scan-security-protocol', path: path.join(ROOT, 'scripts/skyeye/scan-security-protocol.js') } + ]; + + for (const script of scanScripts) { + if (fs.existsSync(script.path)) { + try { + const output = execSync(`node "${script.path}" 2>&1`, { + cwd: ROOT, timeout: 30000, encoding: 'utf8' + }); + // Check for failure indicators in output + if (output.includes('❌') || output.includes('FAIL') || output.includes('ERROR')) { + const failLines = output.split('\n').filter(l => + l.includes('❌') || l.includes('FAIL') || l.includes('ERROR') + ).slice(0, 3); + risks.push({ dimension: 'R7', severity: 'high', file: `(${script.name})`, + detail: `结构扫描 ${script.name} 报告问题: ${failLines.join('; ').slice(0, 200)}` }); + } + } catch (e) { + // Script execution failure is itself a structural concern + const errMsg = (e.stderr || e.message || '').slice(0, 100); + risks.push({ dimension: 'R7', severity: 'high', file: `(${script.name})`, + detail: `结构扫描 ${script.name} 执行失败: ${errMsg}` }); + } + } + } + + return risks; } -// ━━━ 风险评估决策 ━━━ -function makeDecision(risks, touchesSkyeyeZone) { +// ━━━ 风险决策 ━━━ +function makeDecision(risks) { const hasCritical = risks.some(r => r.severity === 'critical'); const highCount = risks.filter(r => r.severity === 'high').length; - // Any critical risk = immediate block - if (hasCritical) { - return { decision: 'block', risk_level: 'critical' }; - } - - // Multiple high risks = block - if (highCount >= 2) { - return { decision: 'block', risk_level: 'high' }; - } - - // Single high risk = warn (allow merge but notify) - if (highCount === 1) { - return { decision: 'warn', risk_level: 'high' }; - } - - // No SkyEye zone touched = safe pass - if (!touchesSkyeyeZone) { - return { decision: 'pass', risk_level: 'low' }; - } - + if (hasCritical) return { decision: 'block', risk_level: 'critical' }; + if (highCount >= 2) return { decision: 'block', risk_level: 'high' }; + if (highCount === 1) return { decision: 'warn', risk_level: 'high' }; return { decision: 'pass', risk_level: 'low' }; } @@ -477,54 +381,31 @@ function run() { console.log(`标题: ${prTitle || '(unknown)'}`); console.log(`时间: ${new Date().toISOString()}`); console.log(''); + console.log('⚖️ 审核模式: 全员全审 — 不区分身份,天眼对所有人一视同仁'); + console.log(''); - // ─── 检测 PR 来源类型 ─── - const fromAgent = isAgentBranch(branch); - const identity = extractAgentIdentity(PR_COMMITS_PATH, prTitle); - const isAgentPR = fromAgent || identity.isAgent; + // ─── 身份识别(仅用于日志/上下文,不影响审核决策)─── + const isAgent = isAgentBranch(branch); + const identity = extractIdentity(PR_COMMITS_PATH, prTitle); + const isAgentPR = isAgent || identity.isAgent; if (isAgentPR) { - console.log('🤖 代理PR检测: 是'); - console.log(` 分支匹配: ${fromAgent ? '是' : '否'}`); - console.log(` Agent标记: ${identity.isAgent ? '是' : '否'}`); - if (identity.agentName) console.log(` 代理名称: ${identity.agentName}`); + console.log('🤖 PR来源: 代理 (Copilot Agent)'); if (identity.devId) console.log(` 开发者编号: ${identity.devId}`); - console.log(` 身份来源: ${identity.source}`); + if (identity.agentName) console.log(` 代理名称: ${identity.agentName}`); } else { - // Non-agent PR from repo owner — this is the owner's own manual work - console.log('👤 主权者手动PR — 天眼合并膜放行'); - console.log(' (非代理分支,无Agent标记)'); - setOutput('decision', 'pass'); - setOutput('risk_level', 'none'); - setOutput('identity_source', 'owner_manual'); - setOutput('risk_summary', '主权者手动PR,天眼合并膜放行'); - process.exit(0); + console.log('👤 PR来源: 手动提交'); } console.log(''); - // ─── 天眼系统完整性预检 ─── - console.log('━━━ 天眼系统完整性预检 ━━━'); - const integrityIssues = verifySkyeyeIntegrity(); - if (integrityIssues.length > 0) { - console.log('⚠️ 天眼系统完整性问题:'); - integrityIssues.forEach(i => console.log(` · ${i}`)); - } else { - console.log('✅ 天眼系统完整性: 正常'); - } - console.log(''); - - // ─── 加载配置和文件 ─── - const config = loadConfig(); + // ─── 加载变更文件 ─── const files = loadPRFiles(); - if (files.length === 0) { console.log('⚠️ 无变更文件或文件列表不可用'); - // For agent PRs with no file info, we block to be safe - console.log('❌ 代理PR无法获取变更文件列表 — 安全起见阻止合并'); + console.log('❌ 无法获取变更文件列表 — 安全起见阻止合并'); setOutput('decision', 'block'); setOutput('risk_level', 'unknown'); - setOutput('identity_source', identity.source); - setOutput('risk_summary', '代理PR变更文件列表不可用'); + setOutput('risk_summary', 'PR变更文件列表不可用'); process.exit(1); } @@ -532,59 +413,33 @@ function run() { files.forEach(f => console.log(` · ${f}`)); console.log(''); - // ─── 查找注册开发者(通过 devId) ─── - const developer = identity.devId ? findDeveloperByDevId(identity.devId, config) : null; - if (developer) { - console.log(`👤 已识别开发者: ${developer.name} (${developer.devId})`); - } else if (identity.devId) { - console.log(`⚠️ 开发者编号 ${identity.devId} 未在门禁系统注册`); - } else { - console.log('⚠️ 无法识别开发者身份'); - } - console.log(''); - - // ─── 分析天眼区域接触情况 ─── - const skyeyeZoneFiles = files.filter(f => isInSkyeyeZone(f)); - const nonSkyeyeFiles = files.filter(f => !isInSkyeyeZone(f)); - const touchesSkyeyeZone = skyeyeZoneFiles.length > 0; - - console.log(`🛡️ 天眼包裹区域文件: ${skyeyeZoneFiles.length}/${files.length}`); - if (touchesSkyeyeZone) { - console.log(' ⚠️ 此PR修改了天眼系统包裹区域 — 启动完整审核'); - skyeyeZoneFiles.forEach(f => console.log(` 🔒 ${f}`)); - } else { - console.log(' ✅ 此PR未触及天眼包裹区域 — 沙箱内操作'); - } - if (nonSkyeyeFiles.length > 0) { - console.log(` 📦 沙箱区域文件: ${nonSkyeyeFiles.length}`); - } - console.log(''); - - // ─── 如果完全在沙箱内且有注册身份 → 放行 ─── - if (!touchesSkyeyeZone && developer) { - console.log('✅ 沙箱内操作 + 已注册开发者 — 天眼合并膜放行'); - setOutput('decision', 'pass'); - setOutput('risk_level', 'low'); - setOutput('identity_source', identity.source); - setOutput('risk_summary', `${developer.name}(${developer.devId}) 沙箱内操作,未触及天眼区域`); - process.exit(0); - } - - // ─── 加载统计 ─── const stats = loadPRStats(); - // ─── 风险检测 ─── - console.log('━━━ 天眼风险检测启动 ━━━'); - const allRisks = [ + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // 天眼全系统审核 — 对所有PR生效,无例外 + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + console.log('━━━ 天眼全系统审核启动 ━━━'); + console.log(''); + + // Phase 1: 风险维度检测 (R1-R6) + console.log('[Phase 1] 风险维度检测 (R1-R6)...'); + const riskChecks = [ ...checkCriticalFiles(files), - ...checkSkyeyeZoneIntrusion(files, developer), ...checkBuildArtifacts(files), - ...checkCoreConfigTampering(files), ...checkMassDeletion(stats), - ...checkWorkflowTampering(files) + ...checkCoreConfigTampering(files, isAgentPR), + ...checkWorkflowTampering(files, isAgentPR), + ...checkPortalFramework(files, isAgentPR) ]; - // Deduplicate by file + dimension + // Phase 2: 仓库结构完整性验证 (R7) + console.log('[Phase 2] 仓库结构完整性验证 (R7)...'); + const structuralRisks = checkStructuralIntegrity(files); + + // Combine all risks + const allRisks = [...riskChecks, ...structuralRisks]; + + // Deduplicate const seen = new Set(); const risks = allRisks.filter(r => { const key = `${r.dimension}:${r.file}`; @@ -593,32 +448,22 @@ function run() { return true; }); - if (risks.length === 0 && !touchesSkyeyeZone) { - console.log('✅ 未检测到风险(沙箱内操作)'); - console.log(''); - console.log('═══ 决策: PASS ═══'); - setOutput('decision', 'pass'); - setOutput('risk_level', 'low'); - setOutput('identity_source', identity.source); - setOutput('risk_summary', '沙箱内操作,未检测到风险'); - process.exit(0); - } + console.log(''); - if (risks.length === 0 && touchesSkyeyeZone) { - // Touches SkyEye zone but no specific risks detected - // This could happen if a registered developer touches their allowed paths - console.log('✅ 天眼区域变更已验证 — 在授权范围内'); + // ─── 结果报告 ─── + if (risks.length === 0) { + console.log('✅ 天眼全系统审核通过'); + console.log(' 风险维度检测: 无风险'); + console.log(' 仓库结构完整性: 正常'); console.log(''); console.log('═══ 决策: PASS ═══'); setOutput('decision', 'pass'); setOutput('risk_level', 'low'); - setOutput('identity_source', identity.source); - setOutput('risk_summary', '天眼区域变更已验证,在授权范围内'); + setOutput('risk_summary', '天眼全系统审核通过'); process.exit(0); } // ─── 输出风险报告 ─── - console.log(''); console.log(`⚠️ 检测到 ${risks.length} 项风险:`); for (const risk of risks) { const icon = risk.severity === 'critical' ? '🔴' : risk.severity === 'high' ? '🟠' : '🟡'; @@ -627,23 +472,21 @@ function run() { console.log(''); // ─── 决策 ─── - const { decision, risk_level } = makeDecision(risks, touchesSkyeyeZone); + const { decision, risk_level } = makeDecision(risks); const summary = risks.map(r => `[${r.dimension}] ${r.detail}`).join(' | '); setOutput('decision', decision); setOutput('risk_level', risk_level); - setOutput('identity_source', identity.source); setOutput('risk_summary', summary.slice(0, 500)); if (decision === 'block') { console.log(`═══ 决策: BLOCK (风险等级: ${risk_level}) ═══`); - console.log('❌ 天眼合并膜: 物理层拒绝合并'); - console.log(' 此PR的变更触及天眼系统包裹区域且未通过审核'); - console.log(' 请联系主权者 TCS-0002∞ 审核后手动合并'); + console.log('❌ 天眼合并膜: 物理切断合并通道'); + console.log(' 此PR不符合仓库整体系统结构要求'); process.exit(1); } else if (decision === 'warn') { console.log(`═══ 决策: WARN (风险等级: ${risk_level}) ═══`); - console.log('⚠️ 天眼合并膜: 存在风险 — 建议主权者审核'); + console.log('⚠️ 天眼合并膜: 存在风险项,建议审核后合并'); process.exit(0); } else { console.log(`═══ 决策: PASS (风险等级: ${risk_level}) ═══`);