Files
nodeagent/docs/DELIVERY-PIPELINE.md
T
jkooe 2d5be044ee chore(privacy): 交付流水线文档去掉其他项目名
`docs/DELIVERY-PIPELINE.md:10` 原以三个真实项目名举例说明「原方案面向 Web/容器
服务」,属审计基线的树内 PII 之一(存量,非本批次引入)。公开仓库不宜暴露项目
组合,改为中性表述「同类多项目实践」,论证语义不变。

注:该处 PII 已存在于既有历史 blob(2026-10-17 起),彻底清除需重写整段历史,
属独立决策,不在本批次范围。
2026-10-26 21:11:43 +08:00

430 lines
24 KiB
Markdown
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.
# nodeagent 交付流水线规范
> 本文是《代码审计 → 上线 完整交付方案》v1.0(2026-10-14)在 **nodeagent** 项目上的落地适配。
> 适配日期:2026-10-17。基线提交:`590f62e`。
---
## 0. 适配原则(先说清楚为什么不能照搬)
原方案面向 **Web/容器服务**(同类多项目实践)——有服务端、有镜像、
有多套环境、有线上流量。nodeagent 与之有三处根本差异,照搬会产生**假门禁**(跑得很好但守不住任何东西)。
因此本文对每一阶段给出三档判定:
| 判定 | 含义 | 处置 |
|---|---|---|
| **适用** | 原样落地即可 | 直接实施 |
| **改造后适用** | 目标一致,但形态不同 | 换等价手段,并说明为什么等价 |
| **不适用** | 本项目不存在该风险面 | **必须给出替代手段**,不允许只写「N/A」了事 |
> **为什么「不适用」也必须给替代**:原方案每项都对应一类真实风险。若某类风险在本项目不存在,
> 说明风险换了形态,替代手段才是真正守它的东西;直接跳过等于承认该风险无人看守。
### nodeagent 项目画像
| 维度 | 事实 |
|---|---|
| 形态 | monorepo:TypeScript(agent/cli/mcp/hub/protocol)+ Rust(Tauri 控制台壳) |
| 交付物 | Windows 安装包 zip(含 `node.exe` + `agent.mjs`)、macOS CLI 三件套、macOS Tauri 控制台 app |
| 运行位置 | 常驻在**用户自己的机器**上(Windows/Linux/macOS),非服务器 |
| 有无服务端 | **无**(点对点直连 + 可选 Hub 中转,Hub 不在本仓库) |
| 有无容器 | **无** |
| 更新方式 | 被控端**拉取式自更新**(`system.agent.update` 读 `latest.json` + sha256) |
| 现有测试 | 单测 **341**(全绿)/ 协议 e2e / Windows 真机 e2e / Rust `cargo test` |
---
## 1. 代码审计(四维扫描 + 漏洞分级)
原方案四维:SAST / SCA / 密钥扫描 / 容器镜像。
| 维度 | 判定 | 现状 | 落地动作 |
|---|---|---|---|
| SAST 静态安全 | **适用** | ❌ 无 | 引入 Semgrep(TS/JS 规则集)进 CI,阻断 High 及以上 |
| SCA 依赖漏洞 | **适用** | ❌ 无 | `pnpm audit --audit-level=high` 进 CI;锁文件已入库 ✅ |
| 密钥与敏感信息 | **适用** | ⚠️ **有巡检无门禁** | 每日定时任务已做隐私巡检(`security-reports/`),但**不阻断合并** → 加 gitleaks 进 CI |
| 容器镜像 | **不适用** | — | 无容器。**替代:交付物断言**(见下) |
### 替代:交付物断言(比镜像扫描更贴本项目的那一层)
nodeagent 的「制品风险」不在镜像层,而在**分发包本身**。已有两道断言,本文将其升格为正式门禁:
1. **PE 架构断言**(`scripts/lib/zip-pe-arch.mjs`):解出最终 zip 内的 `node.exe` 读 PE 头,
与期望架构比对。
- 缘起(2026-10-11 事故):arm64 包里装进了 x64 的 `node.exe`,该包在 ARM Windows 上
**能启动、能 listen,只是 TLS 握手不完成** —— 单测/CI/macOS 侧全都抓不到,
且两个公开包内 `node.exe` 的 sha256 完全相同。
2. **打包期守卫**(`scripts/pack.mjs`):`.ps1` 必须带 UTF-8 BOM、`.cmd` 必须纯 ASCII + CRLF。
这两条各自对应一次真机事故(PowerShell 5.1 解析失败 / CMD 代码页截断)。
> **为什么这两条比通用镜像扫描更要紧**:镜像扫的是「第三方引入的漏洞」,
> 而这两条守的是「我们自己把错误的东西发了出去」——后者是本项目历史上真实发生过的。
### 准入判据(硬门禁)
- Critical / High 漏洞数 **必须为 0**,一票否决。
- Medium 登记并附限期修复计划;Low 可接受但须留痕。
- **黄金法则沿用**:密钥一旦提交即视为泄露,删除提交只是止损,**立即轮换**才是根治。
nodeagent 的密钥面比一般项目大:PSK、Ed25519 私钥、TLS 自签证书 —— 轮换路径已在
`docs/SECURITY.md` §6 应急处置中定义。
---
## 2. 质量门禁(Quality Gate)
| 指标 | 原方案阈值 | 现状 | 落地动作 |
|---|---|---|---|
| 新增代码覆盖率 | ≥ 80% | ❌ **未采集** | `node --test --experimental-test-coverage` 采集,设阈值(见下) |
| 圈复杂度 ≤ 10/方法 | — | ❌ 未检 | 暂不接入(见说明) |
| 重复率 ≤ 3% | — | ❌ 未检 | 暂不接入 |
| 坏味道新增 0 | — | ❌ 未检 | 暂不接入 |
| 阻断性 Bug 0 | — | ✅ `typecheck` + `clippy -D warnings` | 保持 |
**关于覆盖率阈值的现实校准**:原方案 80% 面向新项目。nodeagent 存量 341 个单测集中在
协议/编码/安全守卫,**部分模块(GUI 注入、平台脚本)无法在 CI 上无头验证**。
故采用**增量口径**:
- 全量覆盖率**先只观测、不阻断**(出数字,建立基线);
- **新增/修改文件的行覆盖率 ≥ 70% 才阻断**;
- 无法无头验证的模块显式标注豁免,并在文档中列出理由。
> 直接照搬 80% 全量阈值会导致两种后果:要么长期挂 `--skip`(门禁形同虚设),
> 要么逼迫团队为 GUI 注入代码写无意义的假断言。**门禁的价值在于守得住,不在于数字好看。**
**关于圈复杂度/重复率**:需引入 SonarQube,但对本地优先的单人项目运维成本高于收益,
本文标记为**可选项**,不进阻断门禁。
---
## 3. 自动化测试(金字塔分层)
| 层 | 判定 | 现状 | 说明 |
|---|---|---|---|
| 单元测试 | **适用** | ✅ **341 全绿** | `pnpm test:unit` |
| 集成测试 | **适用** | ✅ 协议 e2e | `pnpm test:e2e`(TLS / ed25519 / 发现 / 重连 / 文件 / Hub) |
| E2E 关键路径 | **适用** | ✅ **Windows 真机 e2e** | `pnpm test:windows`(含 `install.cmd` 双击链路)——**强于原方案**(真机而非模拟) |
| DAST 动态扫描 | **不适用** | — | 无对外 Web 服务面。**替代:协议健壮性测试** |
### 替代:协议健壮性测试(对应「运行态注入」这一类风险)
DAST 守的是「外部输入打进来会怎样」。nodeagent 的对外输入面是 **WebSocket RPC**,
已有等价覆盖(`tests/e2e/run.mjs` 内):
- 畸形 `hello`(protocol 传数字 / 传对象)
- 畸形 `auth`(hmac 传数字 / 对象 + nonce 为 null)
- **未认证直接 invoke**(U-01:必须留审计且不崩)
- 未知能力 / 参数非法 / ACL 拒绝 / 超频
> 这比通用 DAST 更精准:DAST 是黑盒猜测,这里是针对本项目协议状态机的定向攻击面覆盖。
**缺口**:畸形报文目前是**枚举式**用例,未做随机化 fuzz。建议后续加一轮
「随机字节 + 随机 JSON 结构」的模糊测试 → 记为可选改进项。
---
## 4. CI/CD 构建与制品
| 项 | 判定 | 现状 |
|---|---|---|
| 流水线 lint→test→build→scan | **适用** | ⚠️ 有 test/build/typecheck,**缺 scan** |
| 制品仓库 | **改造后适用** | ✅ GitHub Release(非镜像仓库) |
| 版本号 = 语义化 + git SHA | **适用** | ✅ 语义化版本;`release.mjs` 统一写入 11 处 |
| 签名与追溯 | **改造后适用** | ⚠️ 有 sha256 + `latest.json`,**无 cosign/SBOM** |
### 签名与追溯:容器 cosign 不适用,改为「分发包完整性」三件套
cosign 签的是容器镜像。nodeagent 分发的是 zip + `agent.mjs`,等价保障是:
1. **sha256 入库**:`SHA256SUMS.txt` 随包发布(已有)。
2. **包内容清单**:zip 内文件列表与体积随 release 记录,用于事后核对「发出去的到底是什么」。
3. **可验签**:`system.agent.update` **强制校验 sha256**(不校验等于开放远程代码执行)。
这是本项目**最强的一道**——被控端只认哈希,不认「看起来对」的包。
> **待补(可选)**:对 `agent.mjs` 做 detached GPG 签名,供高安全场景人工验签。
> 注意这不能替代 sha256 —— 被控端目前不支持验签,加签名只服务于人工审计场景。
### 现有 CI 的已知待改项
- `retention-days: 30` → 建议 **7**(CI artifact 公开可下载,留存越久暴露面越大)。
- 三个 workflow 均未集成密钥/依赖扫描(见 §1)。
---
## 5. 分级环境发布(dev / staging / prod)
**判定:不适用**,但必须给替代。
nodeagent 无服务端、无多环境:`agent` 跑在用户自己的机器上,控制端是本机 CLI。
「dev/staging/prod」这类环境隔离在物理上不存在。
### 替代:真机矩阵验证
风险换成了形态 —— 不是「哪个环境先炸」,而是「哪类机器先炸」。nodeagent 已多次栽在
**平台差异**上(CRLF、路径分隔符、verbatim 路径、ARM Windows 的 TLS、PowerShell 5.1 的 BOM)。
故替代为**真机矩阵**:
| 平台 | 架构 / 形态 | 验证方式 |
|---|---|---|
| Windows | x64 | `tests/e2e/windows.mjs`(`windows-latest` runner) |
| Windows | **arm64** | 真机验证(CI 无 arm64 Windows runner,必须人工) |
| macOS | CLI 三件套 | 本机安装冒烟 |
| macOS | Tauri 控制台 | `console-windows.yml` 构建 + `verify-bundle.mjs` |
| Linux | systemd 被控端 | `docs/DEPLOY-LINUX.md` + 真机验证 |
> **矩阵里最弱的一环是 Windows arm64** —— CI 覆盖不到,而它恰恰是历史上出过
> 「能启动但 TLS 握手不完成」这一最难发现故障的平台。本文将其列为**发版前必查项**。
---
## 6. 生产发布策略
| 原方案策略 | 判定 | 说明 |
|---|---|---|
| 滚动发布 | 不适用 | 无多实例 |
| 蓝绿部署 | 不适用 | 无流量切换 |
| 金丝雀 | **改造后适用** | 改为**设备分批**:先一台试点 |
| Feature Flag | 适用(有限) | 能力级 ACL 可视为开关 |
### 改造:金丝雀 → 设备分批自更新
被控端是**拉取式**更新(读 `latest.json`),天然支持分批:
1. **试点**:选 1 台可达且物理可及的设备先更新,观察 ≥ 30 分钟。
2. **放量**:确认无异常后,其余设备再触发更新。
3. **回滚**:每设备保留上一版本备份(`agent.mjs.bak-<ts>`)+ `latest.json` 可回退。
### ⚠️ 本项目特有风险:发布即失联(原方案未覆盖)
这是 nodeagent **最严重**的上线失败形态,且原方案完全没有对应条款:
> 一次失败的 agent 更新,会让一台**物理不可达**的远程设备**永久脱管**。
> 对 Web 服务,发布失败可以回滚、可以摘流量;对被控端,**回滚通道本身就是被更新的那个程序**。
缓解措施(部分已有,本文整合为强制项):
| 措施 | 状态 |
|---|---|
| 更新前备份当前 `agent.mjs` | ✅ 已有 |
| **先备份再原子替换**(同目录 rename,失败可回滚) | ✅ 已有 |
| **sha256 强制校验**(不校验不落盘) | ✅ 已有 |
| 更新后**重启并轮询校验新指纹**上线 | ✅ 已有(`deploy` 闭环) |
| **保留「最后已知可用版本」标记(LKG)** | ✅ 已有(`apps/agent/src/lkg.ts`) |
| **控制端手动回滚入口**(`nodeagent rollback`,读 LKG → 覆盖回入口 → 重启 → 复核) | ✅ **v2.1.0 新增**(需被控端可达) |
| **启动期自动恢复**(看门狗:启动连续失败 N 次 → 从 LKG 恢复) | ✅ **v2.1.0 新增**(`scripts/startup-guard.mjs`,三端 supervisor 已接入) |
| **运行期自动回滚**(更新后健康检查失败即自动回滚到备份) | ⚠️ **刻意不做**,见下 |
### LKG 的设计(v2.0.x 落地,v2.1.0 补全量指纹)
- **写入时机**:agent **成功启动后**(服务器已 listen、心跳已起)才写 `agent-lkg.json`。
**不在更新写完时就写** —— 那会把尚未验证的版本当成已知可用,LKG 就失去意义。
- **内容**:`hash`(与 `system.info` 的 `build.hash` 同口径,前 12 位)、
`hash_full`(sha256 **全量 64 位**,v2.1.0 起记录 —— 备份指纹匹配用全值,
消除 12 位前缀碰撞的理论风险)、`bytes`、`script_path`、
`backup_glob`(v2.1.0 起,回滚内容候选的文件名模式 `<basename>.bak*`)、
`recorded_at`、`node_version`、`platform`。
**老记录(无 `hash_full`/`backup_glob`)不猜**:消费方回落 12 位前缀比对与
`<basename>.bak*` 约定,向后兼容。
- **暴露**:`system.info.lkg = { lkg, current_hash, matches, byte_delta }`。
`matches=false` = 当前版本还没被证明能启动 → 控制端应**暂缓向其余设备放量**。
### 回滚通道:手动 rollback + 看门狗自动恢复 + 边界明示(v2.1.0 定稿)
| 场景 | 通道 | 前提 |
|---|---|---|
| 新版本能启动但需回退(行为异常 / 主动避险) | **手动** `nodeagent rollback [--dry-run] [--backup <远端路径>]`:指纹比对定位备份 → 覆盖回入口 → 受控重启 → 复核 `build.hash == LKG` | **被控端可达**(RPC 命令;`--dry-run` 只读零写) |
| 新版本进程**完全起不来**(无监听可连,CLI 救不了) | **看门狗自动恢复** `scripts/startup-guard.mjs`:supervisor 每次拉起先过守卫,启动连续失败 N 次(默认 3)自动从 LKG 恢复入口 | 外层 supervisor 在位(Windows 计划任务 / Linux systemd / macOS launchd,三端均已接入) |
| agent.mjs 自身损坏到 Node 无法加载 / Node 运行时缺失 | **带外恢复**(见附录 C):备份文件 + LKG 文件手工还原 | 物理或远程桌面可达 |
**为什么恢复目标只有 LKG 一个**:回滚循环(回滚 → 启动 → 又判定失败 → 再回滚)在
无人值守设备上比「停在坏版本」更难恢复。看门狗因此只做「回到上一个已知能启动的版本」
这一件事,恢复后计数归零;**不做多版本链式回滚、不做版本选择**。
> **边界(明示,任何文案不得暗示「失联也能一条命令救回」)**:
> ① `nodeagent rollback` 是 RPC 命令,**需被控端可达** —— 它救的是「新版本能启动但需回退」;
> ② 看门狗自身也是 Node 脚本 —— **agent.mjs 损坏到 Node 无法解析、或 Node 运行时缺失时,
> 守卫同样无法运行**,只能带外恢复(附录 C);
> ③ 看门狗只守护「启动」这一时刻;运行期崩溃由外层 supervisor 的重启策略负责
> (systemd `Restart=always` / 计划任务 `RestartCount=10`)。
**看门狗的判据(为什么不会误回滚)**:
- 只计**启动期**死亡(子进程存活 < 30s 宽限期);运行期死亡(计划内重启杀的是长命进程)不计;
- 端口已被别的实例服务 → 判定「使命达成」,退 0 不计数(防 supervisor 重启造成的撞车被误记成启动失败);
- 无 LKG / 无指纹匹配的备份 → **记日志后放行,绝不擦除 install-dir**;
- 计数在 agent **listen 成功后**由 agent 自身归零(与 LKG 同一铁律:不把「启动中」当成功);
- 恢复动作全量记 `<数据目录>/logs/startup-guard.log`(JSONL:时间、动作、命中备份、结果)。
### 为什么**运行期**不做自动回滚(明确的设计取舍,v2.1.0 收窄)
原三问(无法归因 / 回滚循环 / 失控)在**启动期**已被看门狗以受控方式回答
(判据见上:只看启动期死亡、单一 LKG 目标、恢复后归零、全程日志、无匹配不动盘)。
但**运行期**仍维持原取舍、刻意不做:
1. **无法归因**:远程设备上区分不出「更新本身坏了」与「网络抖动 / 端口被占 / 依赖服务未起」。
2. **回滚循环**:见上 —— 运行期自动回滚会把「停一次服务」放大成「来回换版本」。
3. **失控**:被控端一旦在运行期自作主张改自己的程序,控制端就失去对「当前跑的到底是什么」的掌控。
故定位为:**LKG 提供确定的回滚目标;启动期决策交给看门狗(有界、可审计),
运行期决策交给人**(`nodeagent rollback`)。写入期的闭环(备份、哈希校验、写后校验、
不一致即回滚)由 `system.agent.update` 既有实现承担,本文不改动它。
---
## 7. 上线监控与复盘
| 支柱 | 判定 | nodeagent 对应 |
|---|---|---|
| 指标 | **改造后适用** | `system.metrics`:闭环成功率 / P95 时延 / 安全拦截率 / 装软件成功率 |
| 日志 | **适用** | 审计日志 JSONL(哈希链 + 外部锚定),见 `docs/SECURITY.md` §4.8 |
| 链路追踪 | 不适用 | 点对点调用,无分布式链路 |
| 告警 | 改造后适用 | 无值班通道;改为**发版后主动复核** |
### 发布后复核清单(替代 Prometheus 盯盘)
| 项 | 命令 | 期望 |
|---|---|---|
| 能力数未减少 | `nodeagent list` | 与发版前一致 |
| 审计链完整 | `nodeagent audit verify` | `ok: true` |
| 锚点自洽 | `nodeagent audit anchor --compare` | 无「整链重写」告警 |
| 核心能力可用 | `node scripts/verify.mjs --host … --key …` | 全绿 |
| 指标未恶化 | `nodeagent metrics` | 闭环率 ≥ 95%、拦截率 100% |
### 复盘
已有载体:`bug-report-2026-10-13.html`(缺陷台账)、`docs/RETROSPECTIVE.md`(短板复盘)、
`security-reports/YYYY-MM-DD.md`(每日巡检)。
**待补**:原方案要求「任何事故/回滚必须写复盘 —— 时间线、根因、改进项、责任人」。
本项目为单人项目,「责任人」不适用,改为**改进项是否进入守卫**(即:是否变成一条会红的断言)。
> 这是本项目复盘最有价值的一问:**没有变成断言的复盘,等于没有复盘**。
> 历史上多次同类事故复发(CRLF、路径、编码)都栽在这一点上。
---
## 8. 落地清单(Action Checklist)
按优先级排序,✅ 表示本次已落地:
- [x] 建立 `.gitignore` 密钥基线(`PSK.txt` / `keys/` / `*.key` / `*.pem` / `config.json` / `agent.json`)— **已有**
- [x] 密钥扫描(gitleaks)进 CI 并阻断合并 — **本次落地**(`quality-gate.yml`)
- [x] 依赖漏洞扫描(`pnpm audit --audit-level=high`)进 CI — **本次落地**
- [ ] SAST(Semgrep TS/JS 规则集)进 CI — **未做**:需先定规则集,否则噪声会淹没真问题
- [x] 覆盖率采集接入 CI(全量观测 + 变更文件 ≥70% 阻断)— **本次落地**(`scripts/ci/check-coverage.mjs`)
- [x] 提交信息规范强制(Conventional Commits + CI 校验)— **本次落地**(`scripts/ci/check-commit-msg.mjs`)
- [x] 交付物 PE 架构断言 — **已有**,升格为门禁
- [x] 打包期 `.ps1` BOM / `.cmd` ASCII+CRLF 守卫 — **已有**
- [x] CI artifact `retention-days` 30 → 7 — **本次落地**
- [x] 保留「最后已知可用版本」标记(LKG)— **本次落地**
- [ ] 分支保护:main 需 PR + CI 全绿 — **需仓库设置**(当前 protection 返回 404,已知 P1)
- [ ] 发版前 Windows arm64 真机必查(CI 覆盖不到)— **流程项**,已写入本文档
- [ ] 协议模糊测试(随机报文 fuzz)— **可选改进项**
### 本次落地的实测证据
| 项 | 证据 |
|---|---|
| 覆盖率门禁 | 三条故障注入均 `exit=1`(阈值 100 / 全量 99 / lcov 缺失);正常路径 `exit=0` |
| 覆盖率基线 | 全量 **66.9%**(6399/9560 行,54 文件)—— 印证「照搬 80% 全量不现实」 |
| 提交规范校验 | 反例 3 类均红(无类型 / wip / 类型拼错);Merge 与无 scope 正确豁免;**历史 20 条提交一次通过** |
| LKG | 新增 8 条单测全绿;含「篡改 hash 必须检出 matches=false」故障注入 |
| 契约守卫 | 新增 `lkg` 字段后,既有契约测试**立刻报红**(`SystemInfo` 缺声明)→ 补接口后转绿。这本身是门禁有效的实证 |
| 全量单测 | **349 全绿**(落地前 341) |
| 门禁会红的验证 | 覆盖率 3 注入 + LKG 1 注入,均实测为真红,非摆设 |
---
## 附录 A:CI 骨架(GitHub Actions,适配 nodeagent)
```yaml
# .github/workflows/quality-gate.yml
name: Quality Gate
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with: { fetch-depth: 0 }
# 密钥扫描:历史 + 增量(nodeagent 密钥面:PSK / ed25519 / TLS 私钥)
- uses: gitleaks/gitleaks-action@v2
env: { GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} }
# 依赖漏洞
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm audit --audit-level=high
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
# 覆盖率:全量出数字(观测),增量阈值由 coverage-gate 判定
- run: node --test --experimental-test-coverage --test-reporter=lcov \
--test-reporter-destination=coverage/lcov.info "tests/unit/**/*.test.mjs"
- uses: actions/upload-artifact@v4
with: { name: coverage, path: coverage/, retention-days: 7 }
```
> **注**:`windows-e2e.yml` 保留原样 —— 它跑的是真机链路(`install.cmd` / `control.cmd` 双击),
> 是本项目 CI 里最有价值的一环,不可被「更快」的 Linux 流水线替代。
## 附录 B:提交信息规范
项目**实际已在用** Conventional Commits(`git log` 可验证),但**无强制**。补齐校验:
```
<type>(<scope>): <subject>
type: feat | fix | docs | style | refactor | perf | test | chore | ci
scope: agent | cli | mcp | hub | protocol | console | pack | release | linux | …
```
示例(取自实际提交):
```
feat(linux): Linux 被控端适配 —— 安装器 + systemd 自启 + 平台守卫 + 部署文档
fix(console): sidecar 补 CREATE_NO_WINDOW(不再弹常驻黑窗)
chore(privacy): 公开仓库隐私脱敏 —— 清除可定位到个人与设备的字面量
```
## 附录 C:回滚检查清单(nodeagent 版)
**触发回滚的条件(满足任一)**
- 目标设备**失联**(更新后无法连接,且已排除网络问题)—— **最高优先级**
- `nodeagent list` 能力数少于发版前
- `nodeagent audit verify` 返回 `ok: false`
- `verify.mjs` 出现失败项
**回滚动作**
1. **被控端可达**(能连上)→ `nodeagent rollback`(指纹比对定位 LKG 备份 → 覆盖回入口 → 受控重启 → 复核 `build.hash == LKG`;`--dry-run` 可先只读比对)。
2. **被控端起不来**(连不上)→ 看门狗应已介入:启动连续失败 3 次后自动从 LKG 恢复,过程见 `<数据目录>/logs/startup-guard.log`。若看门狗未恢复(无 LKG / 无指纹匹配的备份)或状态异常,登录目标机(物理或带外方式),用备份恢复:`agent.mjs.bak`(或 `agent.mjs.bak-<ts>`)→ `agent.mjs`。
3. 若需回退发布通道:把 `latest.json` 指回上一版本 URL + sha256。
4. 重启 agent(计划任务或 `run-temp.cmd`)。
5. 复核:`verify.mjs` 全绿 → 记录时间线 → 写复盘 → **把根因变成一条会红的断言**。
> 手动回滚与看门狗的分工:rollback 是「人决策、被控端可达」;看门狗是「机器决策、只限启动期」。
> 两者都只认 LKG 一个目标;边界(agent.mjs/Node 损坏时两者都无能为力)见 §6 的明示块。
---
## 变更记录
| 日期 | 内容 |
|---|---|
| 2026-10-17 | 初版。基于《代码审计 → 上线 完整交付方案》v1.0 适配,基线 `590f62e`,单测 341 全绿。 |
| 2026-10-25 | v2.1.0:§6 定稿回滚通道 —— 手动 `nodeagent rollback`(需被控端可达)+ 看门狗自动恢复(`scripts/startup-guard.mjs`,启动连续失败 N 次从 LKG 恢复,三端 supervisor 接入)+ 边界明示(agent.mjs/Node 损坏时守卫也无法运行,只能带外恢复);「运行期自动回滚」维持刻意不做(三问论证收窄到运行期)。LKG 记录补 `hash_full`/`backup_glob` 只增字段(老记录回落 12 位前缀)。附录 C 同步。 |