From 76f1277dcd93a6faad13c191d071b126e9418891 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:15:05 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20=E5=A4=A9=E7=9C=BC?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E8=86=9Cv2=20=E2=80=94=20=E5=90=8C=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=B2=99=E7=AE=B1=E9=9A=94=E7=A6=BB=20+=20Copilot=20A?= =?UTF-8?q?gent=E8=BA=AB=E4=BB=BD=E6=A3=80=E6=B5=8B=20+=20=E7=89=A9?= =?UTF-8?q?=E7=90=86=E5=B1=82=E9=98=BB=E5=A1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/fbf443ce-2f3e-4a23-a3e4-515cc2dda5a7 --- .github/persona-brain/agent-registry.json | 4 +- .github/workflows/skyeye-pr-risk-check.yml | 148 ++++--- scripts/skyeye/pr-risk-check.js | 464 +++++++++++++++------ 3 files changed, 445 insertions(+), 171 deletions(-) diff --git a/.github/persona-brain/agent-registry.json b/.github/persona-brain/agent-registry.json index 1aa5c239..ac45b404 100644 --- a/.github/persona-brain/agent-registry.json +++ b/.github/persona-brain/agent-registry.json @@ -1220,13 +1220,13 @@ }, { "id": "AG-ZY-095", - "name": "PR合并风险检查引擎", + "name": "天眼合并膜引擎", "type": "workflow", "file": ".github/workflows/skyeye-pr-risk-check.yml", "parent_sys": "SYS-GLW-0001", "owner": "ICE-0002∞", "registered": "2026-03-25", - "description": "天眼系统PR级合并前风险评估 — R1关键文件覆盖/R2受保护路径入侵/R3构建产物/R4权限/R5大规模删除" + "description": "天眼系统PR级合并膜 — 同账号沙箱隔离 + 全系统审核。R1关键文件/R2天眼区域入侵/R3构建产物/R4核心配置篡改/R5大规模删除/R6工作流篡改。物理层阻塞:workflow失败=merge按钮禁用" } ], "note": "铸渊已扫描所有workflow文件,按此格式逐一注册,编号从AG-ZY-001开始顺序分配", diff --git a/.github/workflows/skyeye-pr-risk-check.yml b/.github/workflows/skyeye-pr-risk-check.yml index 8cefe905..9cccf093 100644 --- a/.github/workflows/skyeye-pr-risk-check.yml +++ b/.github/workflows/skyeye-pr-risk-check.yml @@ -3,21 +3,33 @@ # 📜 Copyright: 国作登字-2026-A-00037559 # ═══════════════════════════════════════════════ # -# 天眼 · PR 合并风险检查 -# SkyEye PR Merge Risk Check +# 天眼 · 合并膜 (SkyEye Merge Membrane) # -# 弥补天眼系统在 PR 合并环节的安全缺口: -# gate-guard v1/v2 保护 push 事件 -# 本工作流保护 pull_request 事件(合并前风险评估) +# 架构背景: +# 所有开发者共用 qinfendebingshuo 账号(企业权限单人仓库) +# 代理(Copilot Agent)以 copilot/* 分支开发(沙箱隔离) +# 合并到 main 时 → 天眼合并膜自动启动,全系统审核 +# 审核不通过 → 物理层拒绝合并(workflow 失败 = merge 按钮禁用) +# +# 沙箱隔离原则: +# 开发者自由开发(不在天眼包裹内)→ 天眼不干预 +# 触及天眼包裹区域(.github/, scripts/, skyeye/, core/, docs/ 等)→ 必须通过审核 +# 天眼核心配置(security-protocol, gate-guard-config 等)→ 仅限主权者修改 # # 风险检测维度: -# R1 · 关键文件覆盖检测(docs/index.html 等) -# R2 · 受保护路径入侵检测 +# R1 · 关键文件覆盖检测 +# R2 · 天眼包裹区域入侵检测 # R3 · 构建产物误入检测 -# R4 · 开发者路径权限检测 +# R4 · 天眼核心配置篡改检测 # R5 · 大规模删除检测 +# R6 · 工作流篡改检测 # -name: 天眼 · PR风险检查 +# 物理层阻塞: +# 此工作流必须配置为 Required Status Check +# Settings → Branches → Branch protection rules → main → Require status checks +# 添加: "🛡️ 天眼合并膜" 为必需检查 +# +name: 天眼 · 合并膜 on: pull_request: @@ -29,8 +41,8 @@ permissions: pull-requests: write jobs: - risk-check: - name: 🛡️ PR合并风险评估 + merge-membrane: + name: 🛡️ 天眼合并膜 runs-on: ubuntu-latest steps: @@ -44,73 +56,103 @@ jobs: with: node-version: '20' - - name: Collect PR changed files + - name: Collect PR metadata + id: collect env: GH_TOKEN: ${{ github.token }} run: | - # Get the list of files changed in this PR - gh pr diff ${{ github.event.pull_request.number }} --name-only > /tmp/pr_files.txt || true + echo "📂 收集PR变更文件列表..." + gh pr diff ${{ github.event.pull_request.number }} --name-only > /tmp/pr_files.txt + FILE_COUNT=$(wc -l < /tmp/pr_files.txt) + echo " 文件数: $FILE_COUNT" - # Get diff stats - gh pr diff ${{ github.event.pull_request.number }} --stat > /tmp/pr_stats_raw.txt || true - - # Parse stats into numstat-like format for the script - # Extract additions/deletions from the stat output - git diff --numstat origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > /tmp/pr_stats.txt 2>/dev/null || true - - echo "📂 Changed files:" - cat /tmp/pr_files.txt echo "" - echo "📊 Stats:" - cat /tmp/pr_stats.txt + echo "📊 收集变更统计..." + git diff --numstat origin/${{ github.base_ref }}...HEAD > /tmp/pr_stats.txt 2>/dev/null || echo "" > /tmp/pr_stats.txt + + echo "" + echo "📝 收集Commit元数据..." + git log origin/${{ github.base_ref }}..HEAD --format="%B---COMMIT_SEP---" > /tmp/pr_commits.txt 2>/dev/null || echo "" > /tmp/pr_commits.txt + + echo "" + echo "📂 PR变更文件:" + cat /tmp/pr_files.txt + + echo "" + echo "branch=${{ github.head_ref }}" >> "$GITHUB_OUTPUT" - - name: 🔍 天眼 PR 风险检查 - id: risk_check + - name: 🛡️ 天眼合并膜审核 + id: membrane env: PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_BRANCH: ${{ github.head_ref }} + PR_TITLE: ${{ github.event.pull_request.title }} PR_FILES: /tmp/pr_files.txt PR_STATS: /tmp/pr_stats.txt + PR_COMMITS: /tmp/pr_commits.txt run: node scripts/skyeye/pr-risk-check.js - - name: 📝 Write risk report comment + - 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 with: script: | - const fs = require('fs'); - const decision = '${{ steps.risk_check.outcome }}'; - const riskLevel = '${{ steps.risk_check.outputs.risk_level }}' || 'unknown'; - const riskSummary = '${{ steps.risk_check.outputs.risk_summary }}' || ''; + const decision = '${{ steps.membrane.outcome }}'; + const branch = '${{ github.head_ref }}'; const author = '${{ github.event.pull_request.user.login }}'; + const prTitle = context.payload.pull_request.title; + const isAgentBranch = branch.startsWith('copilot/') || branch.startsWith('agent/') || branch.startsWith('bot/'); - let body = '## 🛡️ 天眼 · PR合并风险检查报告\n\n'; - body += `**PR作者:** ${author}\n`; - body += `**检查时间:** ${new Date().toISOString()}\n\n`; + let body = '## 🛡️ 天眼 · 合并膜审核报告\n\n'; + body += `| 项目 | 值 |\n`; + body += `|------|-----|\n`; + body += `| **PR作者** | ${author} |\n`; + body += `| **分支** | \`${branch}\` |\n`; + body += `| **代理PR** | ${isAgentBranch ? '是 🤖' : '否 👤'} |\n`; + body += `| **检查时间** | ${new Date().toISOString()} |\n\n`; if (decision === 'success') { - body += '### ✅ 风险检查通过\n\n'; - body += `风险等级: **${riskLevel}**\n\n`; - if (riskSummary && riskSummary !== '仓库主人 PR,直接放行' && riskSummary !== '白名单用户 PR,直接放行') { - body += '> ⚠️ 注意事项:\n'; - body += `> ${riskSummary}\n\n`; + body += '### ✅ 天眼合并膜: 通过\n\n'; + body += '此 PR 的变更已通过天眼系统审核,可以安全合并。\n\n'; + if (!isAgentBranch) { + body += '> 💡 非代理分支(主权者手动操作),天眼合并膜自动放行\n'; } - body += '此 PR 可以安全合并。\n'; } else { - body += '### ❌ 风险检查未通过\n\n'; - body += `风险等级: **${riskLevel}**\n\n`; - body += '**检测到的风险:**\n'; - if (riskSummary) { - const items = riskSummary.split(' | '); - for (const item of items) { - body += `- 🔴 ${item}\n`; - } - } - body += '\n⛔ **此 PR 已被天眼系统阻止合并。**\n'; - body += '请联系仓库管理员 (TCS-0002∞) 审核。\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 += '\n---\n'; - body += '> 天眼 · PR合并风险检查引擎 · SkyEye PR Risk Check'; + body += '> 天眼 · 合并膜 (SkyEye Merge Membrane) · AG-ZY-095\n'; + body += '> 「地球是圆的,没有缺口 — 语言系统包裹所有入口」'; await github.rest.issues.createComment({ owner: context.repo.owner, diff --git a/scripts/skyeye/pr-risk-check.js b/scripts/skyeye/pr-risk-check.js index 5c9bb6b3..f9f7dca3 100644 --- a/scripts/skyeye/pr-risk-check.js +++ b/scripts/skyeye/pr-risk-check.js @@ -1,37 +1,47 @@ #!/usr/bin/env node // scripts/skyeye/pr-risk-check.js -// 天眼·PR合并风险检查引擎 +// 天眼·合并膜 — PR合并前风险检查引擎 (SkyEye Merge Membrane) // // ═══════════════════════════════════════════════ // 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 // 📜 Copyright: 国作登字-2026-A-00037559 // ═══════════════════════════════════════════════ // -// 目的:弥补天眼系统在PR合并环节的安全缺口 -// gate-guard v1/v2 保护 push 事件 -// 本脚本保护 pull_request 事件(合并前风险评估) +// 架构背景: +// 所有开发者共用 qinfendebingshuo 账号(企业权限单人仓库) +// 代理(Copilot Agent)以 copilot/* 分支开发,通过 PR 合并 +// 无法通过 GitHub username 区分身份 → 改用分支名/commit元数据识别 // -// 核心原则(与 gate-guard 一致): -// 仓库主人(repo_owner)的 PR → 永远放行 -// 系统 bot PR → 永远放行 -// 其他 PR → 按风险等级评估 +// 沙箱隔离原则: +// 开发者在各自分支自由开发(沙箱)→ 天眼不干预 +// 合并到 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 · 受保护路径入侵检测 -// R3 · 构建产物误入检测 -// R4 · 开发者路径权限检测 -// R5 · 大规模删除检测 +// R1 · 关键文件覆盖检测(docs/index.html 等核心文件) +// R2 · 天眼系统包裹区域入侵检测(.github/, scripts/, skyeye/ 等) +// R3 · 构建产物误入检测(Vite/Webpack hash文件) +// R4 · 天眼核心配置篡改检测(security-protocol, gate-guard-config 等) +// R5 · 大规模删除检测(>500行删除) +// R6 · 工作流篡改检测(.github/workflows/ 修改) // // 输入环境变量: -// PR_AUTHOR — PR 作者 GitHub username -// PR_FILES — 变更文件列表路径(默认 /tmp/pr_files.txt) -// PR_STATS — 变更统计路径(默认 /tmp/pr_stats.txt) +// 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) // // 输出: // exit 0 → pass(允许合并) -// exit 1 → block(阻止合并) -// GITHUB_OUTPUT → risk_level, risk_summary, decision +// exit 1 → block(物理层拒绝合并) +// GITHUB_OUTPUT → risk_level, risk_summary, decision, identity_source 'use strict'; @@ -42,39 +52,71 @@ const path = require('path'); const ROOT = path.resolve(__dirname, '../..'); const BRAIN_CONFIG_PATH = path.join(ROOT, '.github/persona-brain/gate-guard-config.json'); const OWNER_CONFIG_PATH = path.join(ROOT, '.github/gate-guard-config.json'); +const SECURITY_PROTOCOL_PATH = path.join(ROOT, '.github/persona-brain/security-protocol.json'); const PR_FILES_PATH = process.env.PR_FILES || '/tmp/pr_files.txt'; 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'; -// ━━━ 关键文件 — 这些文件被覆盖意味着高风险 ━━━ -const CRITICAL_FILES = [ +// ━━━ Copilot Agent 分支模式 ━━━ +const AGENT_BRANCH_PATTERNS = [ + /^copilot\//, // GitHub Copilot agent branches + /^agent\//, // Generic agent branches + /^bot\// // Bot branches +]; + +// ━━━ 天眼系统包裹区域 — 这些路径构成天眼保护的核心系统 ━━━ +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', - '.github/persona-brain/security-protocol.json', - '.github/persona-brain/gate-guard-config.json', - '.github/gate-guard-config.json' + 'package-lock.json' ]; -// ━━━ 构建产物特征 — 这些模式不应出现在仓库中 ━━━ +// ━━━ 天眼核心配置 — 最高保护等级,任何修改都触发 block ━━━ +const SKYEYE_CORE_FILES = [ + '.github/persona-brain/security-protocol.json', + '.github/persona-brain/gate-guard-config.json', + '.github/persona-brain/agent-registry.json', + '.github/persona-brain/ontology.json', + '.github/gate-guard-config.json', + 'scripts/gate-guard.js', + 'scripts/gate-guard-v2.js', + 'scripts/skyeye/pr-risk-check.js' +]; + +// ━━━ 构建产物特征 ━━━ const BUILD_ARTIFACT_PATTERNS = [ - /^docs\/assets\/index-[A-Za-z0-9_-]+\.(js|css)$/, // Vite build hashes - /^dist\//, // dist directory - /^build\//, // build directory - /^\.next\//, // Next.js build - /^node_modules\//, // node_modules - /\.chunk\.(js|css)$/, // Webpack chunks - /\.bundle\.(js|css)$/ // Bundle files + /^docs\/assets\/index-[A-Za-z0-9_-]+\.(js|css)$/, + /^dist\//, + /^build\//, + /^\.next\//, + /^node_modules\//, + /\.chunk\.(js|css)$/, + /\.bundle\.(js|css)$/ ]; // ━━━ 大规模删除阈值 ━━━ -const MASS_DELETE_THRESHOLD = 500; // lines +const MASS_DELETE_THRESHOLD = 500; // ━━━ 安全读取 JSON ━━━ function readJSON(filePath) { @@ -96,6 +138,76 @@ function setOutput(key, value) { } } +// ━━━ 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 || {} @@ -140,7 +253,6 @@ function loadPRStats() { try { if (!fs.existsSync(PR_STATS_PATH)) return null; const content = fs.readFileSync(PR_STATS_PATH, 'utf8').trim(); - // Format: "additions\tdeletions\tfilename" per line let totalDeletions = 0; for (const line of content.split('\n')) { const parts = line.split('\t'); @@ -155,31 +267,43 @@ function loadPRStats() { } } -// ━━━ 识别开发者身份 ━━━ -function identifyDeveloper(author, config) { - // Check brain config +// ━━━ 根据 devId 查找注册开发者 ━━━ +function findDeveloperByDevId(devId, config) { + if (!devId) return null; + if (config.developers_brain) { - for (const [devId, dev] of Object.entries(config.developers_brain)) { - if (dev.github_usernames && dev.github_usernames.includes(author)) { - return { devId, name: dev.name, allowed_paths: dev.allowed_paths || [] }; + for (const [id, dev] of Object.entries(config.developers_brain)) { + if (id === devId) { + return { devId: id, name: dev.name, allowed_paths: dev.allowed_paths || [] }; } } } - // Check owner config - if (config.developers_owner && config.developers_owner[author]) { - const dev = config.developers_owner[author]; - return { devId: dev.dev_id, name: dev.name, allowed_paths: dev.allowed_paths || [] }; + 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 || [] }; + } + } } return null; } +// ━━━ 检查文件是否在天眼包裹区域内 ━━━ +function isInSkyeyeZone(file) { + return SKYEYE_WRAPPED_PATHS.some(p => file === p || file.startsWith(p)); +} + // ━━━ R1 · 关键文件覆盖检测 ━━━ function checkCriticalFiles(files) { const risks = []; + const criticalFiles = [ + 'docs/index.html', 'docs/CNAME', 'docs/.nojekyll', + 'README.md', 'package.json', 'package-lock.json' + ]; for (const file of files) { - if (CRITICAL_FILES.includes(file)) { + if (criticalFiles.includes(file)) { risks.push({ dimension: 'R1', severity: 'high', @@ -191,38 +315,32 @@ function checkCriticalFiles(files) { return risks; } -// ━━━ R2 · 受保护路径入侵检测 ━━━ -function checkProtectedPaths(files, config, developer) { +// ━━━ R2 · 天眼包裹区域入侵检测 ━━━ +function checkSkyeyeZoneIntrusion(files, developer) { const risks = []; - // If developer is registered, check if they're modifying protected paths outside their allowed_paths - if (developer) { - for (const file of files) { - const isProtected = config.system_protected_paths.some(p => file.startsWith(p)); - if (!isProtected) continue; + 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 developer touching protected paths - for (const file of files) { - const isProtected = config.system_protected_paths.some(p => file.startsWith(p)); - if (isProtected) { - risks.push({ - dimension: 'R2', - severity: 'critical', - file, - detail: `未注册开发者修改受保护路径: ${file}` + detail: `${developer.name}(${developer.devId}) 修改了天眼包裹区域: ${file}` }); } + } else { + // Unknown identity touching SkyEye zone + risks.push({ + dimension: 'R2', + severity: 'high', + file, + detail: `代理PR修改天眼包裹区域: ${file}` + }); } } @@ -248,20 +366,16 @@ function checkBuildArtifacts(files) { return risks; } -// ━━━ R4 · 开发者路径权限检测 ━━━ -function checkDeveloperPaths(files, developer) { - if (!developer) return []; // Handled by R2 - +// ━━━ R4 · 天眼核心配置篡改检测 ━━━ +function checkCoreConfigTampering(files) { const risks = []; for (const file of files) { - const isAllowed = developer.allowed_paths.some(p => file.startsWith(p)); - if (!isAllowed) { - // Not in allowed paths — but might be in a non-protected area (info level) + if (SKYEYE_CORE_FILES.includes(file)) { risks.push({ dimension: 'R4', - severity: 'medium', + severity: 'critical', file, - detail: `${developer.name}(${developer.devId}) 修改了非授权路径: ${file}` + detail: `天眼核心配置被修改: ${file} — 此文件仅限 TCS-0002∞ 主权者修改` }); } } @@ -282,90 +396,192 @@ function checkMassDeletion(stats) { return risks; } +// ━━━ R6 · 工作流篡改检测 ━━━ +function checkWorkflowTampering(files) { + 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不应修改自动化管线` + }); + } + } + return risks; +} + +// ━━━ 天眼系统完整性验证 ━━━ +function verifySkyeyeIntegrity() { + const issues = []; + + // Check security protocol + const protocol = readJSON(SECURITY_PROTOCOL_PATH); + if (!protocol) { + issues.push('security-protocol.json 不存在或无法读取'); + } else { + if (!protocol.permanent) issues.push('security-protocol.json: permanent 标记缺失'); + if (!protocol.root_rules || protocol.root_rules.length < 3) { + issues.push('security-protocol.json: root_rules 不完整'); + } + } + + // Check gate-guard configs + if (!readJSON(BRAIN_CONFIG_PATH) && !readJSON(OWNER_CONFIG_PATH)) { + issues.push('门禁配置文件全部缺失'); + } + + return issues; +} + // ━━━ 风险评估决策 ━━━ -function makeDecision(risks) { +function makeDecision(risks, touchesSkyeyeZone) { const hasCritical = risks.some(r => r.severity === 'critical'); const highCount = risks.filter(r => r.severity === 'high').length; - const mediumCount = risks.filter(r => r.severity === 'medium').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' }; } - if (mediumCount >= 3) { - return { decision: 'warn', risk_level: 'medium' }; + + // No SkyEye zone touched = safe pass + if (!touchesSkyeyeZone) { + return { decision: 'pass', risk_level: 'low' }; } + return { decision: 'pass', risk_level: 'low' }; } // ━━━ 主逻辑 ━━━ function run() { const author = process.env.PR_AUTHOR || ''; + const branch = process.env.PR_BRANCH || ''; + const prTitle = process.env.PR_TITLE || ''; + console.log('═══════════════════════════════════════════════'); - console.log('🔺 天眼 · PR合并风险检查引擎'); + console.log('🛡️ 天眼 · 合并膜 (SkyEye Merge Membrane)'); console.log('═══════════════════════════════════════════════'); - console.log(`PR作者: ${author || '(unknown)'}`); + console.log(`PR作者(GitHub): ${author || '(unknown)'}`); + console.log(`分支: ${branch || '(unknown)'}`); + console.log(`标题: ${prTitle || '(unknown)'}`); console.log(`时间: ${new Date().toISOString()}`); console.log(''); - // ─── 白名单检查 ─── - if (author === REPO_OWNER) { - console.log('✅ 仓库主人 PR — 直接放行'); + // ─── 检测 PR 来源类型 ─── + const fromAgent = isAgentBranch(branch); + const identity = extractAgentIdentity(PR_COMMITS_PATH, prTitle); + const isAgentPR = fromAgent || identity.isAgent; + + if (isAgentPR) { + console.log('🤖 代理PR检测: 是'); + console.log(` 分支匹配: ${fromAgent ? '是' : '否'}`); + console.log(` Agent标记: ${identity.isAgent ? '是' : '否'}`); + if (identity.agentName) console.log(` 代理名称: ${identity.agentName}`); + if (identity.devId) console.log(` 开发者编号: ${identity.devId}`); + console.log(` 身份来源: ${identity.source}`); + } 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('risk_summary', '仓库主人 PR,直接放行'); + setOutput('identity_source', 'owner_manual'); + setOutput('risk_summary', '主权者手动PR,天眼合并膜放行'); process.exit(0); } + 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(); - - if (config.whitelist.includes(author)) { - console.log(`✅ 白名单用户 (${author}) — 直接放行`); - setOutput('decision', 'pass'); - setOutput('risk_level', 'none'); - setOutput('risk_summary', '白名单用户 PR,直接放行'); - process.exit(0); - } - - // ─── 加载变更文件 ─── const files = loadPRFiles(); + if (files.length === 0) { - console.log('⚠️ 无变更文件或文件列表不可用 — 放行'); - setOutput('decision', 'pass'); + console.log('⚠️ 无变更文件或文件列表不可用'); + // For agent PRs with no file info, we block to be safe + console.log('❌ 代理PR无法获取变更文件列表 — 安全起见阻止合并'); + setOutput('decision', 'block'); setOutput('risk_level', 'unknown'); - setOutput('risk_summary', '无变更文件信息'); - process.exit(0); + setOutput('identity_source', identity.source); + setOutput('risk_summary', '代理PR变更文件列表不可用'); + process.exit(1); } console.log(`📂 变更文件数: ${files.length}`); files.forEach(f => console.log(` · ${f}`)); console.log(''); - // ─── 识别开发者 ─── - const developer = identifyDeveloper(author, config); + // ─── 查找注册开发者(通过 devId) ─── + const developer = identity.devId ? findDeveloperByDevId(identity.devId, config) : null; if (developer) { - console.log(`👤 识别开发者: ${developer.name} (${developer.devId})`); + console.log(`👤 已识别开发者: ${developer.name} (${developer.devId})`); + } else if (identity.devId) { + console.log(`⚠️ 开发者编号 ${identity.devId} 未在门禁系统注册`); } else { - console.log(`⚠️ 未注册开发者: ${author}`); + 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('━━━ 风险检测开始 ━━━'); + console.log('━━━ 天眼风险检测启动 ━━━'); const allRisks = [ ...checkCriticalFiles(files), - ...checkProtectedPaths(files, config, developer), + ...checkSkyeyeZoneIntrusion(files, developer), ...checkBuildArtifacts(files), - ...checkDeveloperPaths(files, developer), - ...checkMassDeletion(stats) + ...checkCoreConfigTampering(files), + ...checkMassDeletion(stats), + ...checkWorkflowTampering(files) ]; // Deduplicate by file + dimension @@ -377,13 +593,27 @@ function run() { return true; }); - if (risks.length === 0) { - console.log('✅ 未检测到风险'); + if (risks.length === 0 && !touchesSkyeyeZone) { + console.log('✅ 未检测到风险(沙箱内操作)'); console.log(''); console.log('═══ 决策: PASS ═══'); setOutput('decision', 'pass'); setOutput('risk_level', 'low'); - setOutput('risk_summary', '未检测到风险,允许合并'); + setOutput('identity_source', identity.source); + setOutput('risk_summary', '沙箱内操作,未检测到风险'); + process.exit(0); + } + + 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('✅ 天眼区域变更已验证 — 在授权范围内'); + console.log(''); + console.log('═══ 决策: PASS ═══'); + setOutput('decision', 'pass'); + setOutput('risk_level', 'low'); + setOutput('identity_source', identity.source); + setOutput('risk_summary', '天眼区域变更已验证,在授权范围内'); process.exit(0); } @@ -397,21 +627,23 @@ function run() { console.log(''); // ─── 决策 ─── - const { decision, risk_level } = makeDecision(risks); + const { decision, risk_level } = makeDecision(risks, touchesSkyeyeZone); 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('❌ PR被阻止合并 — 请联系仓库管理员审核'); + console.log('❌ 天眼合并膜: 物理层拒绝合并'); + console.log(' 此PR的变更触及天眼系统包裹区域且未通过审核'); + console.log(' 请联系主权者 TCS-0002∞ 审核后手动合并'); process.exit(1); } else if (decision === 'warn') { console.log(`═══ 决策: WARN (风险等级: ${risk_level}) ═══`); - console.log('⚠️ PR存在风险 — 建议管理员审核后合并'); - // Warn exits with 0 to not block, but provides information + console.log('⚠️ 天眼合并膜: 存在风险 — 建议主权者审核'); process.exit(0); } else { console.log(`═══ 决策: PASS (风险等级: ${risk_level}) ═══`);