Files
nodeagent/scripts/anchor-audit.mjs
T
jkooe 54f7f4c7ab test(vm): Parallels Win11 ARM 真机验证 —— A 组 15/0 + v1.5 四项全过 + 锚点端到端通过
## 环境
Parallels 27 + Win11 ARM,VM 10.211.55.9(共享网络);被控端 `-NodeId win-arm -AllowInput`。

## ⭐ B5 结论(与预期不同,但更有价值)
| 组合 | 结果 |
|---|---|
| `win-x64` 包(内含 x64 Node) | ⚠️ **能启动、能 listen 8765,但 TLS 握手 60s 不完成** → 协议层不可用(Prism 下) |
| **换 ARM64 Node(同版本 v22.20.0)** | ✅ **完全正常**:握手成功、连接、51 项能力 |

× 结论:`nodeagent-win-x64.zip` 不能在 ARM Windows 上实用;ARM 上需 ARM64 Node。

## 验证结果
- **A 组(verify.mjs 17 项)**:✅ 15 通过 / 0 失败 / 1 跳过(29.5s)
- **B1 v1.5 四项**:✅ 全过 —— 字符串热键 / 预设别名(`copy→ctrl+c` 映射正确)/ 长按 /
  后台定向投递(**explorer 命中 138 窗口**、WindowsTerminal 10 窗口)
  → **v1.5.0「未经真机就发版」的债,至此还清**
- **B4 锚点端到端**:✅ collect(62 条) → 上传云盘 → verify 判定「链已增长」+ 精确明细
- B2/B3:⏸ 阻塞(需 restart,而计划任务启动失败 —— 手工跑却完全正常)

## 本轮真机抓到的 2 个真 bug(都已修)
1. `control.ps1:100` 的 `$scheme:` 被 PowerShell 当 drive 引用 → **解析期错误 → 整个脚本不可用**
   (stop/start/status 全废);已修 + 加守卫(扫全部 .ps1 的 `$ident:`)
2. `anchor-audit.mjs` 调用**不存在的** `audit head` 子命令 → 改为 `invoke system.audit.head`
   (这种「能力存在但 CLI 无子命令」的错配,不真跑一次发现不了)

## 文档
`docs/VM-VERIFICATION.md` 补「实际验证结果」节 + 5 条新增排查项(安装/密钥/CLI 语法/计划任务)
2026-10-11 00:18:43 +08:00

164 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 审计链外部锚定 —— **控制端**工具(v2.0.0)。
*
* ## 为什么判定要在控制端做
* 被控端是**不可信方**:它若被 root,既会重写 audit.log,也会把自己的锚点一并改掉,
* 并告诉你"一切正常"。所以"锚点存哪、和什么比"必须由**控制端**掌握。
*
* ## 两个子命令
* collect 拉被控端链头 → 生成一条锚点记录(JSONL 一行),供上传到链外
* verify 拉被控端链头 + 读指定锚点文件 → 给出判定结论(三方一致 / 被重写)
*
* ## 判定逻辑不在此处重写
* 复用 `@nodeagent/protocol` 的 `judgeAnchors`(与 agent 端**同一份实现**)——
* 若两端各写一份,同一份日志会出现"被控端说一致、控制端说被改"的荒谬局面。
*
* ## 为什么走 CLI 而不是直接连
* CLI 已经装了连接管理(多设备配置、证书钉住、daemon 连接池),薄薄一层复用即可;
* 自己再实现一遍连接与握手,就是又一次"两份实现"。
*
* 用法:
* node scripts/anchor-audit.mjs collect --node win [--out <文件>] [--note "每日"]
* node scripts/anchor-audit.mjs verify --node win --anchors <锚点文件> [--head-file <链头 JSON>]
*/
import { execFileSync } from 'node:child_process';
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { judgeAnchors, parseAnchorsFromJsonl } from '../packages/protocol/dist/index.js';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
/** 极简参数解析(避免为两个子命令引依赖)。 */
function parseArgs(argv) {
const out = { _: [] };
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i];
if (a.startsWith('--')) {
const k = a.slice(2);
const v = argv[i + 1];
if (v === undefined || v.startsWith('--')) out[k] = true;
else {
out[k] = v;
i += 1;
}
} else out._.push(a);
}
return out;
}
function die(msg, code = 1) {
console.error(`✗ ${msg}`);
process.exit(code);
}
/** 拉取被控端链头(走 CLI,复用其连接管理)。 */
/** 链头来源:优先 `--head-file`(便于离线验证/排查),否则走 CLI。 */
function fetchHead(node, headFile) {
if (headFile && typeof headFile === 'string') {
if (!existsSync(headFile)) die(`链头文件不存在:${headFile}`);
const j = JSON.parse(readFileSync(headFile, 'utf8'));
return j.data ?? j; // 兼容「CLI 信封」与「裸 data」两种写法
}
// ⚠️ CLI 没有 `audit head` 子命令(只有 `audit` 列表与 `audit verify`)——
// system.audit.head 只能走通用 `invoke`。2026-10-11 在 Parallels VM 上真跑时才发现:
// 不实际跑一次,这种"能力存在但 CLI 无子命令"的错配根本暴露不出来。
const args = ['invoke', 'system.audit.head', '--json'];
if (node) args.push('--node', node);
let raw;
try {
raw = execFileSync('nodeagent', args, {
encoding: 'utf8',
cwd: root,
timeout: 60_000,
env: process.env,
});
} catch (err) {
die(`调用 CLI 失败:${err?.message ?? err}(被控端可能离线)`);
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
die(`CLI 输出非 JSON:${String(raw).slice(0, 200)}`);
}
if (parsed?.status !== 'ok' || !parsed.data) {
die(`取链头失败:${parsed?.error?.message ?? '未知错误'}`);
}
return parsed.data;
}
/** 组一条锚点记录(字段与 protocol 的 AnchorRecord 一致)。 */
function toAnchorRecord(head, note) {
return {
ts: Date.now(),
entries: head.entries,
head_hash: head.head_hash ?? null,
head_ts: head.head_ts ?? null,
rotated_segments: head.rotated_segments ?? 0,
...(note ? { note } : {}),
};
}
// ---------------- collect ----------------
function cmdCollect(args) {
const node = args.node;
const head = fetchHead(node, args['head-file']);
const rec = toAnchorRecord(head, typeof args.note === 'string' ? args.note : undefined);
const out = args.out
? String(args.out)
: join(root, 'release', `audit-anchor-${new Date().toISOString().slice(0, 10)}.jsonl`);
mkdirSync(dirname(out), { recursive: true });
if (existsSync(out)) appendFileSync(out, `${JSON.stringify(rec)}\n`, 'utf8');
else writeFileSync(out, `${JSON.stringify(rec)}\n`, 'utf8');
console.log(`✓ 已生成锚点:${rec.entries} 条,链头 ${String(rec.head_hash).slice(0, 16)}…(轮转 ${rec.rotated_segments} 段)`);
console.log(` 文件:${out}`);
console.log(` ⤴ 请把该文件上传到**链外**(网盘/另一台机器)—— 留在本机或与被控端同机都等于没锚。`);
// 便于 automation 直接取用(一行 JSON)
console.log(`ANCHOR_JSON=${JSON.stringify(rec)}`);
return 0;
}
// ---------------- verify ----------------
function cmdVerify(args) {
const node = args.node;
const file = args.anchors;
if (!file || typeof file !== 'string') die('verify 需要 --anchors <锚点文件>(可从网盘下载后传入)');
if (!existsSync(file)) die(`锚点文件不存在:${file}`);
const anchors = parseAnchorsFromJsonl(readFileSync(file, 'utf8'));
if (!anchors) die('锚点文件为空或全部不可解析');
const head = fetchHead(node, args['head-file']);
const cmp = judgeAnchors(head, anchors);
const mark = cmp.ok ? '✓' : '✗';
console.log(`${mark} 判定:${cmp.verdict}`);
console.log(` 锚点条数:${cmp.anchors_checked}(最近一条:${new Date(cmp.latest?.ts ?? 0).toLocaleString()})`);
console.log(` 当前链:${head.entries} 条,链头 ${String(head.head_hash).slice(0, 16)}…,轮转 ${head.rotated_segments} 段`);
if (cmp.detail) console.log(` 明细:${JSON.stringify(cmp.detail)}`);
if (!cmp.ok) {
console.error('');
console.error(' ⚠️ 与链外锚点不一致 —— 需人工核查被控端审计日志是否被重写。');
console.error(' 注意:锚点本身也应有历史(多份),只比最近一条不足以定位被改的区间。');
}
return cmp.ok ? 0 : 2;
}
// ---------------- 入口 ----------------
const args = parseArgs(process.argv.slice(2));
const cmd = args._[0];
const code =
cmd === 'collect' ? cmdCollect(args)
: cmd === 'verify' ? cmdVerify(args)
: (console.error('用法: node scripts/anchor-audit.mjs <collect|verify> --node <设备> [...]'), 1);
process.exit(code);