diff --git a/.github/persona-brain/gate-guard-config.json b/.github/persona-brain/gate-guard-config.json index ff6fd4bd..63fc5891 100644 --- a/.github/persona-brain/gate-guard-config.json +++ b/.github/persona-brain/gate-guard-config.json @@ -11,11 +11,13 @@ "zhuyuan-bot" ], "whitelist_commit_prefixes": [ - "🔍", "📊", "📡", "🧠", "📢", "🚨", "📰", "🔧", "🦅" + "🔍", "📊", "📡", "🧠", "📢", "🚨", "📰", "🔧", "🦅", "🎖️", "🔐", "🌊" ], "signature_exempt_paths": [ "syslog-inbox/", - "data/dc-reports/" + "data/dc-reports/", + "data/bulletin-board/", + "data/security/" ], "system_protected_paths": [ ".github/", diff --git a/.github/workflows/zhuyuan-commander.yml b/.github/workflows/zhuyuan-commander.yml new file mode 100644 index 00000000..39fd50b0 --- /dev/null +++ b/.github/workflows/zhuyuan-commander.yml @@ -0,0 +1,110 @@ +name: "🎖️ 铸渊·将军唤醒 · Commander Wake-up" + +on: + schedule: + - cron: '0 0 * * *' # UTC 00:00 = 北京 08:00 + - cron: '0 12 * * *' # UTC 12:00 = 北京 20:00 + workflow_dispatch: + +permissions: + contents: write + issues: write + actions: read + +jobs: + commander-wakeup: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + # Step 1 · 唤醒核心大脑 + - name: "🧠 Step 1 · 唤醒核心大脑" + id: brain + run: | + echo "[GH-WF-RUN-COMMANDER] 🎖️ 铸渊将军唤醒 · $(date -u +%Y-%m-%dT%H:%M:%SZ)" + BRAIN_OK=true + for f in memory.json routing-map.json dev-status.json; do + if [ -f ".github/persona-brain/$f" ]; then + python3 -c "import json; json.load(open('.github/persona-brain/$f'))" 2>/dev/null \ + && echo "✅ $f 完整" || { echo "❌ $f 损坏"; BRAIN_OK=false; } + else + echo "❌ $f 缺失"; BRAIN_OK=false + fi + done + echo "🌊 加载光湖存在级安全协议..." + if [ -f ".github/persona-brain/security-protocol.json" ]; then + PROTOCOL_LEVEL=$(cat .github/persona-brain/security-protocol.json | python3 -c "import sys,json; print(json.load(sys.stdin)['level'])") + echo "✅ 安全协议已加载 · 等级: ${PROTOCOL_LEVEL}" + else + echo "❌ 安全协议缺失!" + fi + echo "brain_ok=$BRAIN_OK" >> $GITHUB_OUTPUT + + # Step 2 · 全局视图生成 + - name: "📊 Step 2 · 全局视图生成" + id: dashboard + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "[GH-COMMANDER-DASHBOARD] 📊 生成将军全局仪表盘..." + node scripts/commander-dashboard.js + echo "dashboard_ready=true" >> $GITHUB_OUTPUT + + # Step 3 · 读取公告板 + 指纹验证 + - name: "📡 Step 3 · 读取公告板" + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + AGENT_BULLETIN_BOARD_PAGE_ID: ${{ secrets.AGENT_BULLETIN_BOARD_PAGE_ID }} + run: | + echo "[GH-COMMANDER-BULLETIN] 📡 读取Agent集群公告板..." + mkdir -p /tmp/skyeye + node scripts/skyeye/bulletin-board.js > /tmp/skyeye/bulletin-board.json || echo "{}" > /tmp/skyeye/bulletin-board.json + + # Step 4 · SFP 指纹扫描 + - name: "🔐 Step 4 · SFP 指纹扫描" + run: | + echo "[GH-COMMANDER-SFP] 🔐 扫描公告板指纹完整性..." + node scripts/skyeye/scan-bulletin-sfp.js > /tmp/skyeye/sfp-health.json + + # Step 5 · 更新公告栏 + 签到 + 同步 + - name: "📢 Step 5 · 签到 + 更新公告" + run: | + echo "[GH-COMMANDER-CHECKIN] 📢 铸渊将军签到..." + TIMESTAMP=$(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M:%S+08:00') + DATE=$(TZ='Asia/Shanghai' date '+%Y-%m-%d') + + # 将军签到写入 memory.json + python3 -c " + import json, sys + try: + with open('.github/persona-brain/memory.json', 'r') as f: + mem = json.load(f) + mem['commander_last_wakeup'] = '${TIMESTAMP}' + mem['last_updated'] = '$(date -u +%Y-%m-%dT%H:%M:%SZ)' + with open('.github/persona-brain/memory.json', 'w') as f: + json.dump(mem, f, indent=2, ensure_ascii=False) + print('✅ 将军签到已写入 memory.json') + except Exception as e: + print(f'⚠️ 签到写入失败: {e}') + " + + # Step 6 · 提交变更 + - name: "💾 Step 6 · 提交变更" + run: | + git config user.name "zhuyuan-bot" + git config user.email "zhuyuan@guanghulab.com" + git add data/bulletin-board/dashboard.json .github/persona-brain/memory.json + if ! git diff --cached --quiet; then + git commit -m "🎖️ 铸渊将军唤醒 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M') · 仪表盘更新" + git push + else + echo "ℹ️ 无变更需要提交" + fi diff --git a/.github/workflows/zhuyuan-gate-guard.yml b/.github/workflows/zhuyuan-gate-guard.yml index 104b7f6c..30e442d5 100644 --- a/.github/workflows/zhuyuan-gate-guard.yml +++ b/.github/workflows/zhuyuan-gate-guard.yml @@ -65,7 +65,7 @@ jobs: fi # 系统前缀 commit → 直接放行 - SYSTEM_PREFIXES="🔍|📊|📡|🧠|📢|🚨|📰|🔧|🦅" + SYSTEM_PREFIXES="🔍|📊|📡|🧠|📢|🚨|📰|🔧|🦅|🎖️|🔐|🌊" if echo "$COMMIT_MSG" | head -1 | grep -qE "^($SYSTEM_PREFIXES)"; then echo "[GH-GATE-ALLOW] ℹ️ 系统前缀 commit,直接放行" echo "is_system=true" >> $GITHUB_OUTPUT diff --git a/.github/workflows/zhuyuan-skyeye.yml b/.github/workflows/zhuyuan-skyeye.yml index 79d8b505..f4f1778c 100644 --- a/.github/workflows/zhuyuan-skyeye.yml +++ b/.github/workflows/zhuyuan-skyeye.yml @@ -76,6 +76,9 @@ jobs: node scripts/skyeye/scan-brain-health.js > /tmp/skyeye/brain-health.json node scripts/skyeye/scan-external-bridges.js > /tmp/skyeye/bridge-health.json node scripts/skyeye/scan-security-protocol.js > /tmp/skyeye/security-health.json + node scripts/commander-dashboard.js > /dev/null 2>&1 || true + node scripts/skyeye/scan-bulletin-sfp.js > /tmp/skyeye/sfp-health.json + node scripts/skyeye/scan-soldier-health.js > /tmp/skyeye/soldier-health.json - name: "🔬 Phase 3 · 诊断" id: diagnose diff --git a/data/bulletin-board/comments/.gitkeep b/data/bulletin-board/comments/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/bulletin-board/config-shares/.gitkeep b/data/bulletin-board/config-shares/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/bulletin-board/dashboard.json b/data/bulletin-board/dashboard.json new file mode 100644 index 00000000..b8c23c9e --- /dev/null +++ b/data/bulletin-board/dashboard.json @@ -0,0 +1,323 @@ +{ + "commander": "铸渊", + "timestamp": "2026-03-19 00:21:26+08:00", + "global_view": { + "total_soldiers": 60, + "healthy": 0, + "failed": 0, + "needs_optimization": 60, + "soldiers": [ + { + "name": "🤖 铸渊 · Discussion 自动回复", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🧊 冰朔人格体 · 自动部署诊断", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "冰朔主控神经系统 · 自动维护", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 Brain Sync", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 🌉 桥接·广播PDF生成+分发", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Bridge E · GitHub Changes → Notion", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 🌉 桥接·心跳检测", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "Generate Session Summary for Notion", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 🌉 桥接·SYSLOG 上行处理", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Bridge A · SYSLOG → Notion", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "模块结构检查", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🔧 铸渊 · Daily Maintenance Agent", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "📰 铸渊 · 光湖开发日报", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🌀 部署铸渊聊天室 (GitHub Pages)", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🚀 铸渊 CD · 自动部署到 guanghulab.com", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 广播分发", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · ESP 邮件信号处理器(已暂停)", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "📡 铸渊 · 执行层状态同步", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 飞书SYSLOG桥接", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 光湖纪元 模块文档自动生成", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "HLI Contract Check", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🐕 元看门狗 · 巡检健康监控", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "Notion Callback Pipeline", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Notion 连通性测试", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "Notion Heartbeat Monitor", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Notion 页面阅读器", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Notion 工单轮询", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "📡 铸渊 · Notion Agent 唤醒监听", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🔄 OpenClaw · 唤醒闭环", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "Persona Invoke Endpoint", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🔧 铸渊 · PM2 服务诊断与健康检查", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "Process Notion Work Orders", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🌊 Persona Studio · 代码生成", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🌊 Persona Studio · 对话处理", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🌊 Persona Studio · 完成通知", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🌊 Persona Studio · 登录校验", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · PSP 分身巡检", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 广播推送飞书(聊天消息)", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Push Broadcast · Notion → 飞书文档B", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🏠 Sandbox Deploy", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🔍 Server Patrol · 服务器每日巡检", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🔍 铸渊预演部署 (Staging Preview)", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "📝 Sync Deploy Status to Notion", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "📡 铸渊 · dev-status 自动同步", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Sync Login Entry · Notion → 飞书文档A", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🔄 铸渊跨仓库同步 · persona-studio", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "SYSLOG Auto Pipeline", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "📡 SYSLOG Issue Pipeline", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · SYSLOG Pipeline (A/D/E)", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🧪 Notion Bridge Connectivity Test", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "📢 更新系统公告区", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 图书馆目录自动更新", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Brain Sync", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🤖 铸渊巡检 Agent · 每日自动巡检与修复", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · 每日自检", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🚨 铸渊·智能门禁 · Push Gate Guard", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · Issue 自动回复", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "铸渊 · PR Review", + "status": "⚠️", + "last_run": "unknown" + }, + { + "name": "🦅 铸渊·天眼 · 全局俯瞰 + 自动诊断 + 修复驱动", + "status": "⚠️", + "last_run": "unknown" + } + ] + }, + "bulletin_board_status": { + "unread_work_orders": 0, + "pending_receipts": [] + }, + "skyeye_latest": { + "overall_health": "🟡", + "report_id": "SKYEYE-20260316", + "issues": 6, + "auto_fixed": 6 + }, + "decision": "优化 60 个小兵" +} \ No newline at end of file diff --git a/data/bulletin-board/receipts/.gitkeep b/data/bulletin-board/receipts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/bulletin-board/work-orders/.gitkeep b/data/bulletin-board/work-orders/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/security/sfp-alert-log.json b/data/security/sfp-alert-log.json new file mode 100644 index 00000000..e406c84b --- /dev/null +++ b/data/security/sfp-alert-log.json @@ -0,0 +1,5 @@ +{ + "protocol_version": "SFP-v1.0", + "description": "SFP安全警报日志", + "alerts": [] +} diff --git a/data/security/sfp-config.json b/data/security/sfp-config.json new file mode 100644 index 00000000..1a2928db --- /dev/null +++ b/data/security/sfp-config.json @@ -0,0 +1,33 @@ +{ + "protocol_version": "SFP-v1.0", + "hash_algorithm": "SHA-256-first-12", + "nonce_length": 6, + "trusted_agents": [ + { + "agent_id": "AG-SY", + "persona_chain": "PER-SY001←TCS-0002∞", + "name": "霜砚", + "side": "notion" + }, + { + "agent_id": "AG-ZY", + "persona_chain": "PER-ZY001←YM-GL∞", + "name": "铸渊", + "side": "github" + }, + { + "agent_id": "AG-QQ", + "persona_chain": "GEN∞-BB-QIUQIU←TCS-2025∞", + "name": "秋秋", + "side": "notion" + }, + { + "agent_id": "AG-TY", + "persona_chain": "AG-TY-01←YM-GL∞", + "name": "天眼", + "side": "both" + } + ], + "auto_clean": true, + "alert_on_invalid": true +} diff --git a/data/security/sfp-nonce-registry.json b/data/security/sfp-nonce-registry.json new file mode 100644 index 00000000..255deaf3 --- /dev/null +++ b/data/security/sfp-nonce-registry.json @@ -0,0 +1,162 @@ +{ + "protocol_version": "SFP-v1.0", + "description": "已使用nonce记录(防重放攻击)", + "used_nonces": [ + { + "nonce": "mzrnx9", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:20:38+08:00", + "content_hash": "ca079781b83f" + }, + { + "nonce": "5awo5s", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:21:19+08:00", + "content_hash": "ca079781b83f" + }, + { + "nonce": "totoee", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "05a17114410b" + }, + { + "nonce": "48ayc4", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "d6cd1c87b887" + }, + { + "nonce": "1hw1z8", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "0a5df5415ade" + }, + { + "nonce": "sggt21", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "c3b54c6c243e" + }, + { + "nonce": "l9ibu1", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "c3b54c6c243e" + }, + { + "nonce": "57uay4", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "b95bfcf4a347" + }, + { + "nonce": "lwfc7x", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "10c4ec3aeafb" + }, + { + "nonce": "bar266", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "902e455fe109" + }, + { + "nonce": "o0nt8j", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "929460febd09" + }, + { + "nonce": "41ck6l", + "agent_id": "AG-SY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "f85462954167" + }, + { + "nonce": "oecf7l", + "agent_id": "AG-QQ", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "c10beff06c0b" + }, + { + "nonce": "6ykmfo", + "agent_id": "AG-TY", + "timestamp": "2026-03-19T00:22:17+08:00", + "content_hash": "7f4b1a99e8fc" + }, + { + "nonce": "agjir2", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "05a17114410b" + }, + { + "nonce": "tvydx3", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "d6cd1c87b887" + }, + { + "nonce": "tn8p7x", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "0a5df5415ade" + }, + { + "nonce": "5fx7xl", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "c3b54c6c243e" + }, + { + "nonce": "faym9x", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "c3b54c6c243e" + }, + { + "nonce": "rvdfmy", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "b95bfcf4a347" + }, + { + "nonce": "72pm2d", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "10c4ec3aeafb" + }, + { + "nonce": "f194ff", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "902e455fe109" + }, + { + "nonce": "kym6oy", + "agent_id": "AG-ZY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "929460febd09" + }, + { + "nonce": "ekbzcz", + "agent_id": "AG-SY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "f85462954167" + }, + { + "nonce": "wbjbs7", + "agent_id": "AG-QQ", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "c10beff06c0b" + }, + { + "nonce": "qktryb", + "agent_id": "AG-TY", + "timestamp": "2026-03-19T00:22:24+08:00", + "content_hash": "7f4b1a99e8fc" + } + ] +} \ No newline at end of file diff --git a/scripts/commander-dashboard.js b/scripts/commander-dashboard.js new file mode 100644 index 00000000..d7c1c15a --- /dev/null +++ b/scripts/commander-dashboard.js @@ -0,0 +1,191 @@ +// scripts/commander-dashboard.js +// 铸渊·将军全局仪表盘生成器 +// +// 功能: +// ① 拉取所有 Workflow 运行状态 +// ② 读取天眼最新报告 +// ③ 读取公告板状态 +// ④ 生成将军全局仪表盘 JSON +// +// 输出:data/bulletin-board/dashboard.json + stdout + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const BRAIN_DIR = path.join(ROOT, '.github/persona-brain'); +const SKYEYE_REPORTS_DIR = path.join(ROOT, 'data/skyeye-reports'); +const BULLETIN_DIR = path.join(ROOT, 'data/bulletin-board'); +const DASHBOARD_PATH = path.join(BULLETIN_DIR, 'dashboard.json'); + +const BEIJING_OFFSET_MS = 8 * 3600 * 1000; + +// ━━━ 安全读取 JSON ━━━ +function readJSON(filePath) { + try { + if (!fs.existsSync(filePath)) return null; + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +// ━━━ 获取最新天眼报告 ━━━ +function getLatestSkyeyeReport() { + try { + if (!fs.existsSync(SKYEYE_REPORTS_DIR)) return null; + const files = fs.readdirSync(SKYEYE_REPORTS_DIR) + .filter(f => f.startsWith('skyeye-') && f.endsWith('.json')) + .sort() + .reverse(); + if (files.length === 0) return null; + return readJSON(path.join(SKYEYE_REPORTS_DIR, files[0])); + } catch (e) { + return null; + } +} + +// ━━━ 获取公告板工单状态 ━━━ +function getBulletinBoardStatus() { + const workOrdersDir = path.join(BULLETIN_DIR, 'work-orders'); + const receiptsDir = path.join(BULLETIN_DIR, 'receipts'); + + let unreadWorkOrders = 0; + const pendingReceipts = []; + + try { + if (fs.existsSync(workOrdersDir)) { + const orders = fs.readdirSync(workOrdersDir) + .filter(f => f.endsWith('.json')); + for (const orderFile of orders) { + const order = readJSON(path.join(workOrdersDir, orderFile)); + if (order && order.status !== 'completed') { + unreadWorkOrders++; + pendingReceipts.push(order.order_id || orderFile.replace('.json', '')); + } + } + } + } catch (e) { + // ignore + } + + return { + unread_work_orders: unreadWorkOrders, + pending_receipts: pendingReceipts + }; +} + +// ━━━ 从天眼报告提取小兵状态 ━━━ +function extractSoldiersFromReport(report) { + const soldiers = []; + + if (!report) return { soldiers, total: 0, healthy: 0, failed: 0, needs_optimization: 0 }; + + // 从 workflow_health 提取小兵 + if (report.workflow_health && report.workflow_health.details) { + for (const wf of report.workflow_health.details) { + const status = wf.status === 'healthy' ? '✅' : + wf.status === 'failed' ? '❌' : '⚠️'; + const soldier = { + name: wf.name || wf.file, + status, + last_run: wf.last_run || 'unknown' + }; + if (status === '❌') { + soldier.error = wf.error || 'unknown error'; + soldier.fix_plan = 'retry + check logs'; + } + soldiers.push(soldier); + } + } + + const total = soldiers.length; + const healthy = soldiers.filter(s => s.status === '✅').length; + const failed = soldiers.filter(s => s.status === '❌').length; + const needsOpt = soldiers.filter(s => s.status === '⚠️').length; + + return { soldiers, total, healthy, failed, needs_optimization: needsOpt }; +} + +// ━━━ 生成决策摘要 ━━━ +function generateDecision(soldierStats, bulletinStatus, skyeyeReport) { + const actions = []; + + if (soldierStats.failed > 0) { + actions.push(`修复 ${soldierStats.failed} 个故障小兵`); + } + if (soldierStats.needs_optimization > 0) { + actions.push(`优化 ${soldierStats.needs_optimization} 个小兵`); + } + if (bulletinStatus.unread_work_orders > 0) { + actions.push(`执行 ${bulletinStatus.unread_work_orders} 个未完成工单`); + } + if (skyeyeReport && skyeyeReport.diagnosis && skyeyeReport.diagnosis.needs_human > 0) { + actions.push(`${skyeyeReport.diagnosis.needs_human} 个问题需人工处理`); + } + + if (actions.length === 0) { + return '全局健康 · 无需干预 · 继续巡航'; + } + return actions.join(' → '); +} + +// ━━━ 主函数 ━━━ +function generateDashboard() { + const now = new Date(); + const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString() + .replace('T', ' ').slice(0, 19) + '+08:00'; + + // 读取天眼最新报告 + const skyeyeReport = getLatestSkyeyeReport(); + + // 提取小兵状态 + const soldierStats = extractSoldiersFromReport(skyeyeReport); + + // 读取公告板状态 + const bulletinStatus = getBulletinBoardStatus(); + + // 生成决策 + const decision = generateDecision(soldierStats, bulletinStatus, skyeyeReport); + + const dashboard = { + commander: '铸渊', + timestamp: bjTime, + global_view: { + total_soldiers: soldierStats.total, + healthy: soldierStats.healthy, + failed: soldierStats.failed, + needs_optimization: soldierStats.needs_optimization, + soldiers: soldierStats.soldiers + }, + bulletin_board_status: bulletinStatus, + skyeye_latest: skyeyeReport ? { + overall_health: skyeyeReport.overall_health || '❓', + report_id: skyeyeReport.report_id || null, + issues: skyeyeReport.diagnosis ? skyeyeReport.diagnosis.total_issues : 0, + auto_fixed: skyeyeReport.diagnosis ? skyeyeReport.diagnosis.auto_fixed : 0 + } : { + overall_health: '❓', + report_id: null, + issues: 0, + auto_fixed: 0 + }, + decision + }; + + // 保存仪表盘 + fs.mkdirSync(BULLETIN_DIR, { recursive: true }); + fs.writeFileSync(DASHBOARD_PATH, JSON.stringify(dashboard, null, 2)); + + console.log(JSON.stringify(dashboard, null, 2)); + console.log(`\n📊 将军全局仪表盘已生成 · ${bjTime}`); +} + +// 允许作为模块导入或直接运行 +if (require.main === module) { + generateDashboard(); +} + +module.exports = { generateDashboard }; diff --git a/scripts/sfp-core.js b/scripts/sfp-core.js new file mode 100644 index 00000000..f5f7d629 --- /dev/null +++ b/scripts/sfp-core.js @@ -0,0 +1,257 @@ +// scripts/sfp-core.js +// 系统指纹安全协议 · SFP v1.0 · 核心模块 +// +// 功能: +// ① generateSFP(agent_id, content) — 生成系统指纹 +// ② verifySFP(content_with_signature) — 验证系统指纹 +// ③ loadConfig() — 加载 SFP 配置 +// +// 指纹格式: ⌜SFP::{agent_id}::{persona_chain}::{timestamp}::{content_hash}::{nonce}⌝ + +'use strict'; + +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const SFP_CONFIG_PATH = path.join(ROOT, 'data/security/sfp-config.json'); +const SFP_NONCE_PATH = path.join(ROOT, 'data/security/sfp-nonce-registry.json'); +const SFP_ALERT_PATH = path.join(ROOT, 'data/security/sfp-alert-log.json'); + +const BEIJING_OFFSET_MS = 8 * 3600 * 1000; + +// ━━━ SFP 指纹正则 ━━━ +const SFP_REGEX = /⌜SFP::([^:]+)::([^:]+)::([0-9T+:.-]+)::([a-f0-9]{12})::([a-zA-Z0-9]{6})⌝/; + +// ━━━ 安全读取 JSON ━━━ +function readJSON(filePath) { + try { + if (!fs.existsSync(filePath)) return null; + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +// ━━━ 安全写入 JSON ━━━ +function writeJSON(filePath, data) { + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); +} + +// ━━━ 加载 SFP 配置 ━━━ +function loadConfig() { + const config = readJSON(SFP_CONFIG_PATH); + if (!config) { + throw new Error('SFP配置文件缺失: ' + SFP_CONFIG_PATH); + } + return config; +} + +// ━━━ 查找受信Agent ━━━ +function findTrustedAgent(config, agentId) { + if (!config || !config.trusted_agents) return null; + return config.trusted_agents.find(a => a.agent_id === agentId) || null; +} + +// ━━━ 计算内容哈希(SHA-256前12位) ━━━ +function computeContentHash(content) { + const hash = crypto.createHash('sha256').update(content, 'utf8').digest('hex'); + return hash.slice(0, 12); +} + +// ━━━ 生成6位随机nonce ━━━ +function generateNonce() { + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + let nonce = ''; + const bytes = crypto.randomBytes(6); + for (let i = 0; i < 6; i++) { + nonce += chars[bytes[i] % chars.length]; + } + return nonce; +} + +// ━━━ 获取北京时间ISO字符串 ━━━ +function getBeijingTimestamp() { + const now = new Date(); + const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS); + return bjTime.toISOString().replace('Z', '+08:00').replace(/\.\d{3}/, ''); +} + +// ━━━ 生成系统指纹 ━━━ +function generateSFP(agentId, content) { + const config = loadConfig(); + const agent = findTrustedAgent(config, agentId); + + if (!agent) { + return { + success: false, + error: 'AGENT_NOT_FOUND', + message: `Agent ${agentId} 不在受信Agent列表中` + }; + } + + const timestamp = getBeijingTimestamp(); + const contentHash = computeContentHash(content); + const nonce = generateNonce(); + + const fingerprint = `⌜SFP::${agent.agent_id}::${agent.persona_chain}::${timestamp}::${contentHash}::${nonce}⌝`; + + // 记录 nonce 到已使用列表 + try { + const nonceRegistry = readJSON(SFP_NONCE_PATH) || { used_nonces: [] }; + nonceRegistry.used_nonces.push({ + nonce, + agent_id: agentId, + timestamp, + content_hash: contentHash + }); + // 保留最近 1000 条 nonce 记录 + if (nonceRegistry.used_nonces.length > 1000) { + nonceRegistry.used_nonces = nonceRegistry.used_nonces.slice(-1000); + } + writeJSON(SFP_NONCE_PATH, nonceRegistry); + } catch (e) { + // nonce 记录失败不阻断指纹生成 + } + + return { + success: true, + fingerprint, + signed_content: `${content}\n${fingerprint}`, + meta: { + agent_id: agent.agent_id, + persona_chain: agent.persona_chain, + timestamp, + content_hash: contentHash, + nonce + } + }; +} + +// ━━━ 验证系统指纹 ━━━ +function verifySFP(contentWithSignature) { + const config = loadConfig(); + + // Step 1: 检查是否有指纹块 + const match = contentWithSignature.match(SFP_REGEX); + if (!match) { + return { + valid: false, + error: 'NO_FINGERPRINT', + level: '⚠️', + message: '无指纹·不可信', + log: `[SFP-WARN] 无指纹内容发现` + }; + } + + const [, agentId, personaChain, timestamp, contentHash, nonce] = match; + + // Step 2: 验证 agent_id + const agent = findTrustedAgent(config, agentId); + if (!agent) { + return { + valid: false, + error: 'INVALID_AGENT', + level: '❌', + message: `无效Agent ID: ${agentId}`, + log: `[SFP-ALERT] 伪造Agent ID: ${agentId}` + }; + } + + // Step 3: 验证亲子链 + if (agent.persona_chain !== personaChain) { + return { + valid: false, + error: 'CHAIN_MISMATCH', + level: '❌', + message: `亲子链伪造: 期望 ${agent.persona_chain},实际 ${personaChain}`, + log: `[SFP-CRITICAL] 亲子链伪造企图 · agent=${agentId}` + }; + } + + // Step 4: 验证内容哈希 + // 从内容中去除指纹行,重新计算哈希 + const fingerprintLine = match[0]; + const originalContent = contentWithSignature + .replace(fingerprintLine, '') + .replace(/\n$/, '') + .trim(); + const recomputedHash = computeContentHash(originalContent); + + if (recomputedHash !== contentHash) { + return { + valid: false, + error: 'CONTENT_TAMPERED', + level: '❌', + message: `内容被篡改: hash不匹配 (期望=${contentHash}, 实际=${recomputedHash})`, + log: `[SFP-CRITICAL] 内容篡改检测 · hash不匹配` + }; + } + + // Step 5: 检查 nonce 重放 + const nonceRegistry = readJSON(SFP_NONCE_PATH) || { used_nonces: [] }; + const nonceUsed = nonceRegistry.used_nonces.some( + n => n.nonce === nonce && n.agent_id !== agentId + ); + // 注意:同一个 agent 的相同 nonce 是自己生成的,允许验证 + // 只有不同 agent 使用相同 nonce 才是重放攻击 + const duplicateNonce = nonceRegistry.used_nonces.filter(n => n.nonce === nonce); + if (duplicateNonce.length > 1) { + // 检查是否有不同 agent 使用了相同 nonce + const uniqueAgents = new Set(duplicateNonce.map(n => n.agent_id)); + if (uniqueAgents.size > 1) { + return { + valid: false, + error: 'REPLAY_ATTACK', + level: '❌', + message: `重放攻击: nonce ${nonce} 被多个Agent使用`, + log: `[SFP-CRITICAL] 重放攻击 · nonce重复 · nonce=${nonce}` + }; + } + } + + return { + valid: true, + level: '✅', + message: '指纹验证通过', + meta: { + agent_id: agentId, + persona_chain: personaChain, + timestamp, + content_hash: contentHash, + nonce, + agent_name: agent.name, + agent_side: agent.side + } + }; +} + +// ━━━ 记录安全警报 ━━━ +function logAlert(alertEntry) { + try { + const alertLog = readJSON(SFP_ALERT_PATH) || { alerts: [] }; + alertLog.alerts.push({ + ...alertEntry, + logged_at: getBeijingTimestamp() + }); + // 保留最近 500 条 + if (alertLog.alerts.length > 500) { + alertLog.alerts = alertLog.alerts.slice(-500); + } + writeJSON(SFP_ALERT_PATH, alertLog); + } catch (e) { + console.error('[SFP] 写入警报日志失败:', e.message); + } +} + +module.exports = { + generateSFP, + verifySFP, + loadConfig, + findTrustedAgent, + computeContentHash, + generateNonce, + logAlert, + SFP_REGEX +}; diff --git a/scripts/skyeye/report-generator.js b/scripts/skyeye/report-generator.js index 2eaec3e2..e92dd92c 100644 --- a/scripts/skyeye/report-generator.js +++ b/scripts/skyeye/report-generator.js @@ -40,6 +40,8 @@ function generateReport() { const brainHealth = readJSON(path.join(SKYEYE_DIR, 'brain-health.json')); const bridgeHealth = readJSON(path.join(SKYEYE_DIR, 'bridge-health.json')); const securityHealth = readJSON(path.join(SKYEYE_DIR, 'security-health.json')); + const sfpHealth = readJSON(path.join(SKYEYE_DIR, 'sfp-health.json')); + const soldierHealth = readJSON(path.join(SKYEYE_DIR, 'soldier-health.json')); const diagnosis = readJSON(path.join(SKYEYE_DIR, 'diagnosis.json')); const repairResult = readJSON(path.join(SKYEYE_DIR, 'repair-result.json')); @@ -111,6 +113,28 @@ function generateReport() { last_verified: bjTime }, + bulletin_sfp_health: sfpHealth ? { + dimension: 'D14', + status: sfpHealth.status || '❓', + total_messages: sfpHealth.total_messages || 0, + valid_fingerprints: sfpHealth.valid_fingerprints || 0, + invalid_fingerprints: sfpHealth.invalid_fingerprints || 0, + without_fingerprint: sfpHealth.without_fingerprint || 0, + sfp_config_exists: sfpHealth.sfp_config_exists || false, + agent_activity: sfpHealth.agent_activity || {} + } : { dimension: 'D14', status: '❓' }, + + soldier_health: soldierHealth ? { + dimension: 'D15', + status: soldierHealth.status || '❓', + total_soldiers: soldierHealth.total_soldiers || 0, + healthy: soldierHealth.healthy || 0, + failed: soldierHealth.failed || 0, + failure_rate: soldierHealth.failure_rate || 0, + recurring_errors: soldierHealth.recurring_errors || [], + recommendations: soldierHealth.recommendations || [] + } : { dimension: 'D15', status: '❓' }, + diagnosis: { total_issues: diagnosis ? diagnosis.total_issues : 0, auto_fixed: repairResult ? repairResult.total_repaired : 0, @@ -164,6 +188,16 @@ function generateReport() { } else if (!report.security_health.copyright_anchor) { report.next_actions.push('P1: 安全协议版权锚点缺失,需补充'); } + if (report.bulletin_sfp_health && report.bulletin_sfp_health.invalid_fingerprints > 0) { + report.next_actions.push('P0: 公告板发现无效指纹内容,需立即清理'); + } + if (report.soldier_health && report.soldier_health.failure_rate > 20) { + report.next_actions.push('P0: 小兵故障率超过20%,需全局修复'); + } + if (report.soldier_health && report.soldier_health.recurring_errors && + report.soldier_health.recurring_errors.length > 0) { + report.next_actions.push('P1: 存在持续性故障小兵,建议铸渊介入'); + } // 保存完整报告 fs.mkdirSync(SKYEYE_DIR, { recursive: true }); diff --git a/scripts/skyeye/scan-bulletin-sfp.js b/scripts/skyeye/scan-bulletin-sfp.js new file mode 100644 index 00000000..baacf1c6 --- /dev/null +++ b/scripts/skyeye/scan-bulletin-sfp.js @@ -0,0 +1,163 @@ +// scripts/skyeye/scan-bulletin-sfp.js +// 天眼·扫描模块D14 · 公告板指纹完整性检查 +// +// 扫描内容: +// ① 扫描 data/bulletin-board/ 下所有留言 +// ② 检查每条留言是否携带有效 SFP 指纹 +// ③ 统计无指纹/无效指纹内容数 +// ④ 统计各Agent留言活跃度 +// +// 输出:JSON → stdout + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); +const BULLETIN_DIR = path.join(ROOT, 'data/bulletin-board'); +const SECURITY_DIR = path.join(ROOT, 'data/security'); +const SFP_CONFIG_PATH = path.join(SECURITY_DIR, 'sfp-config.json'); + +const SFP_REGEX = /⌜SFP::([^:]+)::([^:]+)::([0-9T+:.-]+)::([a-f0-9]{12})::([a-zA-Z0-9]{6})⌝/; + +// ━━━ 安全读取 JSON ━━━ +function readJSON(filePath) { + try { + if (!fs.existsSync(filePath)) return null; + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +// ━━━ 扫描目录中的 JSON 文件内容 ━━━ +function scanDirectory(dirPath) { + const results = []; + try { + if (!fs.existsSync(dirPath)) return results; + const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json')); + for (const file of files) { + const filePath = path.join(dirPath, file); + const content = fs.readFileSync(filePath, 'utf8'); + results.push({ file, path: filePath, content }); + } + } catch (e) { + // ignore + } + return results; +} + +// ━━━ 检查内容中的 SFP 指纹 ━━━ +function checkFingerprint(content, config) { + const match = content.match(SFP_REGEX); + if (!match) { + return { has_fingerprint: false, valid: false, reason: '无指纹' }; + } + + const [, agentId, personaChain] = match; + + // 检查 agent 是否在受信列表 + const agent = (config.trusted_agents || []).find(a => a.agent_id === agentId); + if (!agent) { + return { has_fingerprint: true, valid: false, reason: `无效Agent ID: ${agentId}` }; + } + + // 检查亲子链 + if (agent.persona_chain !== personaChain) { + return { has_fingerprint: true, valid: false, reason: `亲子链不匹配: ${agentId}` }; + } + + return { has_fingerprint: true, valid: true, agent_id: agentId, agent_name: agent.name }; +} + +// ━━━ D14 主扫描 ━━━ +function scanBulletinSFP() { + const BEIJING_OFFSET_MS = 8 * 3600 * 1000; + const now = new Date(); + const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString() + .replace('T', ' ').slice(0, 19) + '+08:00'; + + const config = readJSON(SFP_CONFIG_PATH) || { trusted_agents: [] }; + + const result = { + dimension: 'D14', + name: '公告板指纹完整性', + scan_time: bjTime, + status: '✅', + total_messages: 0, + with_fingerprint: 0, + without_fingerprint: 0, + valid_fingerprints: 0, + invalid_fingerprints: 0, + agent_activity: {}, + issues: [], + sfp_config_exists: fs.existsSync(SFP_CONFIG_PATH) + }; + + // 检查 SFP 配置是否存在 + if (!result.sfp_config_exists) { + result.status = '❌'; + result.issues.push({ + type: 'config_missing', + message: 'SFP配置文件缺失', + action: 'P0工单 · 恢复sfp-config.json' + }); + console.log(JSON.stringify(result, null, 2)); + return result; + } + + // 扫描公告板各区域 + const areas = ['comments', 'config-shares', 'receipts', 'work-orders']; + for (const area of areas) { + const dirPath = path.join(BULLETIN_DIR, area); + const files = scanDirectory(dirPath); + + for (const file of files) { + // 跳过 .gitkeep + if (file.file === '.gitkeep') continue; + + result.total_messages++; + const check = checkFingerprint(file.content, config); + + if (check.has_fingerprint) { + result.with_fingerprint++; + if (check.valid) { + result.valid_fingerprints++; + // 统计活跃度 + const agentId = check.agent_id; + result.agent_activity[agentId] = (result.agent_activity[agentId] || 0) + 1; + } else { + result.invalid_fingerprints++; + result.issues.push({ + type: 'invalid_fingerprint', + file: file.file, + area, + reason: check.reason, + action: 'P0工单 · 清理无效指纹内容' + }); + } + } else { + result.without_fingerprint++; + result.issues.push({ + type: 'no_fingerprint', + file: file.file, + area, + action: '标记待清理' + }); + } + } + } + + // 确定整体状态 + if (result.invalid_fingerprints > 0) { + result.status = '❌'; + } else if (result.without_fingerprint > 0) { + result.status = '⚠️'; + } + + console.log(JSON.stringify(result, null, 2)); + return result; +} + +scanBulletinSFP(); diff --git a/scripts/skyeye/scan-soldier-health.js b/scripts/skyeye/scan-soldier-health.js new file mode 100644 index 00000000..6a83c3e8 --- /dev/null +++ b/scripts/skyeye/scan-soldier-health.js @@ -0,0 +1,153 @@ +// scripts/skyeye/scan-soldier-health.js +// 天眼·扫描模块D15 · 小兵健康全局视图 +// +// 扫描内容: +// ① 读取铸渊将军仪表盘 commander-dashboard.json +// ② 小兵故障率 > 20% → P0 工单 +// ③ 小兵连续7天同一错误未修复 → P1 工单 +// ④ 全局小兵健康统计 +// +// 输出:JSON → stdout + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); +const DASHBOARD_PATH = path.join(ROOT, 'data/bulletin-board/dashboard.json'); +const SKYEYE_REPORTS_DIR = path.join(ROOT, 'data/skyeye-reports'); + +// ━━━ 安全读取 JSON ━━━ +function readJSON(filePath) { + try { + if (!fs.existsSync(filePath)) return null; + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +// ━━━ 获取最近N天的天眼报告 ━━━ +function getRecentReports(days) { + const reports = []; + try { + if (!fs.existsSync(SKYEYE_REPORTS_DIR)) return reports; + const files = fs.readdirSync(SKYEYE_REPORTS_DIR) + .filter(f => f.startsWith('skyeye-') && f.endsWith('.json')) + .sort() + .reverse() + .slice(0, days); + + for (const file of files) { + const report = readJSON(path.join(SKYEYE_REPORTS_DIR, file)); + if (report) reports.push(report); + } + } catch (e) { + // ignore + } + return reports; +} + +// ━━━ D15 主扫描 ━━━ +function scanSoldierHealth() { + const BEIJING_OFFSET_MS = 8 * 3600 * 1000; + const now = new Date(); + const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString() + .replace('T', ' ').slice(0, 19) + '+08:00'; + + const result = { + dimension: 'D15', + name: '小兵健康全局视图', + scan_time: bjTime, + status: '✅', + dashboard_exists: false, + total_soldiers: 0, + healthy: 0, + failed: 0, + needs_optimization: 0, + failure_rate: 0, + issues: [], + recurring_errors: [], + recommendations: [] + }; + + // 读取将军仪表盘 + const dashboard = readJSON(DASHBOARD_PATH); + if (!dashboard) { + result.dashboard_exists = false; + result.status = '⚠️'; + result.issues.push({ + type: 'dashboard_missing', + message: '将军仪表盘不存在 · 需先运行 commander-dashboard.js', + action: 'P1工单 · 生成仪表盘' + }); + } else { + result.dashboard_exists = true; + const gv = dashboard.global_view || {}; + result.total_soldiers = gv.total_soldiers || 0; + result.healthy = gv.healthy || 0; + result.failed = gv.failed || 0; + result.needs_optimization = gv.needs_optimization || 0; + + // 计算故障率 + if (result.total_soldiers > 0) { + result.failure_rate = Math.round((result.failed / result.total_soldiers) * 100); + } + + // 故障率 > 20% → P0 + if (result.failure_rate > 20) { + result.status = '❌'; + result.issues.push({ + type: 'high_failure_rate', + message: `小兵故障率 ${result.failure_rate}% > 20%`, + action: 'P0工单 · 全局小兵修复' + }); + } + } + + // 检查连续7天同一错误 + const recentReports = getRecentReports(7); + if (recentReports.length >= 2) { + // 收集各报告中的失败 workflow + const errorMap = {}; + for (const report of recentReports) { + if (report.workflow_health && report.workflow_health.details) { + for (const wf of report.workflow_health.details) { + if (wf.status === 'failed') { + const key = wf.name || wf.file; + errorMap[key] = (errorMap[key] || 0) + 1; + } + } + } + } + + // 连续7天同一错误 + for (const [name, count] of Object.entries(errorMap)) { + if (count >= 7) { + result.recurring_errors.push({ + soldier: name, + consecutive_failures: count, + action: 'P1工单 · 建议铸渊介入' + }); + if (result.status === '✅') result.status = '⚠️'; + } + } + } + + // 生成建议 + if (result.failed > 0) { + result.recommendations.push(`修复 ${result.failed} 个故障小兵`); + } + if (result.recurring_errors.length > 0) { + result.recommendations.push(`${result.recurring_errors.length} 个小兵存在持续性故障,需铸渊介入`); + } + if (result.total_soldiers === 0) { + result.recommendations.push('仪表盘无小兵数据 · 需先生成仪表盘'); + } + + console.log(JSON.stringify(result, null, 2)); + return result; +} + +scanSoldierHealth(); diff --git a/scripts/skyeye/skyeye-main.js b/scripts/skyeye/skyeye-main.js index b8bafedd..37fb31e8 100644 --- a/scripts/skyeye/skyeye-main.js +++ b/scripts/skyeye/skyeye-main.js @@ -72,6 +72,8 @@ function main() { results.brain = runModule('scan-brain-health.js', 'brain-health.json'); results.bridges = runModule('scan-external-bridges.js', 'bridge-health.json'); results.security = runModule('scan-security-protocol.js', 'security-health.json'); + results.sfp = runModule('scan-bulletin-sfp.js', 'sfp-health.json'); + results.soldiers = runModule('scan-soldier-health.js', 'soldier-health.json'); // Phase 3: 诊断 console.log('\n🔬 Phase 3 · 诊断'); diff --git a/tests/contract/sfp-core.test.js b/tests/contract/sfp-core.test.js new file mode 100644 index 00000000..296a980b --- /dev/null +++ b/tests/contract/sfp-core.test.js @@ -0,0 +1,155 @@ +// tests/contract/sfp-core.test.js +// SFP v1.0 · 系统指纹安全协议 · 契约测试 + +'use strict'; + +const path = require('path'); + +// ━━━ 加载 SFP 核心模块 ━━━ +const sfpCore = require(path.resolve(__dirname, '../../scripts/sfp-core.js')); + +describe('SFP v1.0 · 系统指纹安全协议', () => { + + describe('配置加载', () => { + test('loadConfig() 应成功加载配置', () => { + const config = sfpCore.loadConfig(); + expect(config).toBeDefined(); + expect(config.protocol_version).toBe('SFP-v1.0'); + expect(config.trusted_agents).toBeInstanceOf(Array); + expect(config.trusted_agents.length).toBeGreaterThanOrEqual(4); + }); + + test('受信Agent列表包含 AG-ZY(铸渊)', () => { + const config = sfpCore.loadConfig(); + const zy = config.trusted_agents.find(a => a.agent_id === 'AG-ZY'); + expect(zy).toBeDefined(); + expect(zy.name).toBe('铸渊'); + expect(zy.side).toBe('github'); + }); + + test('受信Agent列表包含 AG-SY(霜砚)', () => { + const config = sfpCore.loadConfig(); + const sy = config.trusted_agents.find(a => a.agent_id === 'AG-SY'); + expect(sy).toBeDefined(); + expect(sy.name).toBe('霜砚'); + expect(sy.persona_chain).toBe('PER-SY001←TCS-0002∞'); + }); + }); + + describe('指纹生成 · generateSFP()', () => { + test('已知Agent生成指纹成功', () => { + const result = sfpCore.generateSFP('AG-ZY', '测试内容'); + expect(result.success).toBe(true); + expect(result.fingerprint).toMatch(/^⌜SFP::/); + expect(result.fingerprint).toMatch(/⌝$/); + expect(result.meta.agent_id).toBe('AG-ZY'); + expect(result.meta.content_hash).toHaveLength(12); + expect(result.meta.nonce).toHaveLength(6); + }); + + test('未知Agent生成指纹失败', () => { + const result = sfpCore.generateSFP('AG-FAKE', '测试内容'); + expect(result.success).toBe(false); + expect(result.error).toBe('AGENT_NOT_FOUND'); + }); + + test('不同内容产生不同哈希', () => { + const r1 = sfpCore.generateSFP('AG-ZY', '内容A'); + const r2 = sfpCore.generateSFP('AG-ZY', '内容B'); + expect(r1.success).toBe(true); + expect(r2.success).toBe(true); + expect(r1.meta.content_hash).not.toBe(r2.meta.content_hash); + }); + + test('每次生成不同的nonce', () => { + const r1 = sfpCore.generateSFP('AG-ZY', '相同内容'); + const r2 = sfpCore.generateSFP('AG-ZY', '相同内容'); + expect(r1.meta.nonce).not.toBe(r2.meta.nonce); + }); + + test('signed_content 包含原始内容和指纹', () => { + const content = '铸渊留言测试'; + const result = sfpCore.generateSFP('AG-ZY', content); + expect(result.signed_content).toContain(content); + expect(result.signed_content).toContain(result.fingerprint); + }); + }); + + describe('指纹验证 · verifySFP()', () => { + test('正确指纹验证通过', () => { + const gen = sfpCore.generateSFP('AG-ZY', '正确内容测试'); + const verify = sfpCore.verifySFP(gen.signed_content); + expect(verify.valid).toBe(true); + expect(verify.level).toBe('✅'); + expect(verify.meta.agent_name).toBe('铸渊'); + }); + + test('无指纹内容 → NO_FINGERPRINT', () => { + const verify = sfpCore.verifySFP('纯文本没有指纹'); + expect(verify.valid).toBe(false); + expect(verify.error).toBe('NO_FINGERPRINT'); + expect(verify.level).toBe('⚠️'); + }); + + test('内容被篡改 → CONTENT_TAMPERED', () => { + const gen = sfpCore.generateSFP('AG-ZY', '原始内容'); + const tampered = gen.signed_content.replace('原始内容', '篡改内容'); + const verify = sfpCore.verifySFP(tampered); + expect(verify.valid).toBe(false); + expect(verify.error).toBe('CONTENT_TAMPERED'); + expect(verify.level).toBe('❌'); + }); + + test('伪造Agent ID → INVALID_AGENT', () => { + // 手动构造一个伪造的指纹 + const fakeContent = '伪造内容\n⌜SFP::AG-FAKE::FAKE-CHAIN::2026-03-19T00:00:00+08:00::abcdef123456::x7k9m2⌝'; + const verify = sfpCore.verifySFP(fakeContent); + expect(verify.valid).toBe(false); + expect(verify.error).toBe('INVALID_AGENT'); + expect(verify.level).toBe('❌'); + }); + + test('亲子链不匹配 → CHAIN_MISMATCH', () => { + // 使用正确的 agent_id 但错误的 persona_chain + const hash = sfpCore.computeContentHash('伪造亲子链'); + const fakeContent = `伪造亲子链\n⌜SFP::AG-ZY::FAKE-CHAIN::2026-03-19T00:00:00+08:00::${hash}::x7k9m2⌝`; + const verify = sfpCore.verifySFP(fakeContent); + expect(verify.valid).toBe(false); + expect(verify.error).toBe('CHAIN_MISMATCH'); + expect(verify.level).toBe('❌'); + }); + + test('所有4个Agent都能生成和验证', () => { + const agents = ['AG-ZY', 'AG-SY', 'AG-QQ', 'AG-TY']; + for (const agentId of agents) { + const gen = sfpCore.generateSFP(agentId, `${agentId} 测试`); + expect(gen.success).toBe(true); + const verify = sfpCore.verifySFP(gen.signed_content); + expect(verify.valid).toBe(true); + } + }); + }); + + describe('工具函数', () => { + test('computeContentHash 返回12位hex', () => { + const hash = sfpCore.computeContentHash('测试内容'); + expect(hash).toHaveLength(12); + expect(hash).toMatch(/^[a-f0-9]{12}$/); + }); + + test('generateNonce 返回6位字母数字', () => { + const nonce = sfpCore.generateNonce(); + expect(nonce).toHaveLength(6); + expect(nonce).toMatch(/^[a-z0-9]{6}$/); + }); + + test('SFP_REGEX 能正确匹配指纹格式', () => { + const fp = '⌜SFP::AG-ZY::PER-ZY001←YM-GL∞::2026-03-19T00:00:00+08:00::abcdef123456::x7k9m2⌝'; + const match = fp.match(sfpCore.SFP_REGEX); + expect(match).not.toBeNull(); + expect(match[1]).toBe('AG-ZY'); + expect(match[4]).toBe('abcdef123456'); + expect(match[5]).toBe('x7k9m2'); + }); + }); +});