docs: 增量 PRD / ARCH 与设计图入库(v2.1.0 全面完善)

本轮「全面完善」的四份设计产出此前后在 git 外(untracked)——
阶段 3 实现所依据的需求/架构/类图/时序图随收口一并入库,
使「代码 ← 设计」的引用链在仓库内闭环(代码注释已按 ARCH 任务号引用)。
This commit is contained in:
jkooe committed 2026-10-26 01:01:32 +08:00
1 parent 0074d09f08
commit a7827ba986
4 files changed
+853

No files matched your search

@@ -0,0 +1,570 @@
# ARCH-2026-10-25:nodeagent「全面完善」—— 代码/架构审查 + 增量设计 + 任务分解
> **文档性质**:阶段 2 产出(架构师高见远)。基线 `3680218`(v2.0.3)。
> 范围权威定义:`docs/PRD-2026-10-25-comprehensive-improvement.md`(需求方已批准:P0+P1 全做、P2 推后、版本 v2.1.0、selfupdate 出网走方案 A、L2 同期限 MCP+控制台)。
> 工程铁律来源:项目记忆(契约三段 / 错误规范 / 边界 / Windows 陷阱 / 守卫铁律 / LKG 时机 / 审计哈希口径 / 建文件禁 git add -A / 播种兼容 / git push 代理)。
> **所有结论均有 `文件:行号` 证据**;凡未找到证据的一律进「待确认」不写成事实。
---
## Part 0:审查方法与基线事实
### 0.1 审查范围(实读文件)
| 区域 | 文件 |
|---|---|
| 被控端 | `apps/agent/src/index.ts`、`server.ts`、`audit.ts`、`lkg.ts`、`config.ts`、`events.ts`、`discovery.ts`、`hub-mode.ts`、`capabilities/{index,system,selfupdate,audit-l2,fs,task,monitor,input,window,screen,record,clipboard,app,network,audio,log,await}.ts`、`util/{exec,ps-helper,quote,dpi,platform}.ts` |
| 协议 | `packages/protocol/src/{validate,acl,errors,messages,methods,net,ed25519,hmac,hotkeys,ulid,audit-types,audit-anchor}.ts`、`capabilities/{names,schemas}.ts`、`capabilities/manifest/*.ts` |
| 控制端 | `apps/cli/src/{index,options,core,help,types}.ts`、`cmd/*.ts`;`apps/mcp/src/index.ts`、`tools/defs.ts`;`apps/console/src/**`、`src-tauri/src/agent.rs`(节选);`sidecar/{main,proxy}.mjs` |
| 脚本/测试 | `scripts/lib/version-write.mjs`、`scripts/uninstall.ps1`、`install.ps1`(节选);`tests/unit/*`(43 个)、`tests/e2e/{run,windows}.mjs`(节选) |
### 0.2 基线实测(复扫确认,PRD §1.1 数据属实)
| 项 | 值 | 取证 |
|---|---|---|
| 能力数 | 52 | manifest 实点(51 + `system.audit.l2`,`names.ts:53`) |
| MCP 工具 | 41 | `apps/mcp/src/tools/defs.ts` 实点(`mcp-tools.test.mjs:25` 断言 41) |
| 单测 | 357 | PRD 实跑记载(本轮未重跑,以 PRD 为基线) |
| e2e | 33 + Windows 13 | `tests/e2e/run.mjs` + `windows.mjs` |
| L2 在 MCP/控制台 | 0 命中 | `apps/mcp/src grep l2` = 0;`apps/console/src grep l2` = 0 |
| CLI rollback | 不存在 | `apps/cli/src grep rollback` 仅 net.ts 类型注释;`index.ts` 无 case |
| L2 returns_schema | 空壳 | `manifest/system.ts:321` `returns_schema: { type: 'object' }` |
| selfupdate 裸 fetch | 存在 | `selfupdate.ts:42` |
---
## Part A:代码/架构审查报告(问题清单)
> 严重度定义:**P0 = 安全/正确性缺陷**(含 fail-open 守卫缺陷、可无线利用的崩溃/误伤);**P1 = 重要缺口**(功能缺口、观测污染、性能热点、可维护性硬伤);**P2 = 优化**。
> 「新」= PRD 需求池之外本轮新发现。
### A.1 P0(4 条新发现 + 2 条对 PRD 已知项的补充证据)
#### A-P0-1(新)参数校验的 `additionalProperties:false` 可被原型键绕过 —— fail-open 方向
- **证据**:`packages/protocol/src/validate.ts:69` `if (!(key in schema.properties))`。
- **机理**:`in` 运算符沿原型链查找。`schema.properties` 是普通对象字面量,继承 `Object.prototype`。调用方传入 `{"constructor": 1}`、`{"toString": 1}`、`{"valueOf": 1}` 等键时,`key in properties` 为 `true` → **不判 additional property** → 校验放行。
- **触发条件**:任何已认证调用方对**任何**能力传上述键(manifest 大量能力声明了 `additionalProperties: false`,如 `manifest/system.ts:24`)。
- **现状危害评估**:当前各 handler 均按名读取已知键,多余键被忽略,**暂无直接可利用路径**;但这是「fail-open 方向的守卫缺陷」—— 与本仓刚修过的 B-03 是**同一错误模式在不同文件复发**:`server.ts:228` 已用 `Object.prototype.hasOwnProperty.call(cfg.keys, clientId)` 修过「`client_id:"constructor"` 沿原型链取到函数当密钥打死进程」。
- **修复方向**:改为 `Object.prototype.hasOwnProperty.call(schema.properties, key)`;补单测「传 `constructor` 键必须被判 additional property」(守卫要会红:注入故障 = 改回 `in`,测试必须失败)。
#### A-P0-2(新)限速桶 key 用「限速值」而非「能力名」—— 同上限值的能力互相挤占;桶只增不删
- **证据**:`apps/agent/src/server.ts:364` `allowByRate(state.clientId ?? '', rateLimit, String(rateLimit ?? ''))` —— 第三参 `bucket` 传的是 `String(rateLimit)`(限速值本身);`server.ts:101-113` 桶内只过滤当次 60s 窗口,**key 永久留存**(`rateBuckets` Map 无任何清扫)。
- **机理一(正确性)**:ACL 支持按能力限速(`rateLimitFor(clientRule, name)`,精确 > glob > 全局)。当某 client 配了「screen.capture ≤ 5/min、app.install ≤ 5/min」两条同值规则时,两者 key 都是 `clientId|5` → **共用一个桶**:截图 5 次后装软件也被限速(误伤),且两者总量仍按 5/min(漏过)。
- **机理二(可用性/内存)**:psk 模式下 `client_id` 由客户端自报(`server.ts:295` 注释自陈「配置级约束」)→ 外部可枚举任意 client_id 发起调用 → `rateBuckets` 键无界增长(每个键一个数组,长期运行即内存泄漏);且没有限速配置时 `rateLimit` 为 undefined → `String(undefined)` = `"undefined"` → 所有无差别调用共用一个 key(此时 `allowByRate` 第一行 `if (!limitPerMin) return true` 直接放行,不写桶,故只在前一种情况受害)。
- **修复方向**:key 改 `${clientId}|${name}`(能力名);加桶清扫(每次 `allowByRate` 顺手删过期键,或 5 分钟定时);给桶总数设上限(如 10_000,超出淘汰最旧)。单测:同值两能力互不干扰;过期键被清。
#### A-P0-3(新,PRD P0-2 的前提缺口)LKG 只有指纹没有「内容指针」,且 selfupdate 备份永不清理
- **证据**:`apps/agent/src/lkg.ts:34-44` `LkgRecord` 只有 `hash`(12 位)/`bytes`/`script_path`/`recorded_at`/`node_version`/`platform` —— **没有任何字段指向「这个版本的内容在哪」**。而 `docs/DELIVERY-PIPELINE.md:233` 承诺「`nodeagent rollback` 读 LKG → 覆盖回入口 → 重启」却从未定义内容来源。
- **备份现状(两套命名、一个不清理)**:
- 推送式 deploy:固定名 `<target>.bak`,每次部署**覆盖**(`apps/cli/src/cmd/ops.ts:279`,注释记载「真机堆了 15+ 个 bak」后改覆盖);
- 拉取式 selfupdate:`<script>.bak-<ts>`(`apps/agent/src/capabilities/selfupdate.ts:162`),**永不清理** —— `ops.ts:336-351` 的清理命令只针对 `${target}.bak-*` 且由 CLI 在部署时触发;selfupdate 路径自己不清,也没有任何其他地方清。
- 后果:多次自更新后目录里堆 N 个 ~1MB 备份(deploy 踩过的坑在 selfupdate 路径复发),且**回滚时无法自动判断哪个备份是 LKG**。
- **修复方向**:见 B-P0-2(rollback 用「枚举备份 + 指纹匹配」定位内容;LkgRecord 增加 `hash_full` 与 `backup_glob` 只增字段;selfupdate 补「保留最近 3 个」清理)。
#### A-P0-4(新)`describeArgs` 对大参数同步序列化,阻塞被控端事件循环
- **证据**:`apps/agent/src/audit.ts:172-177` `JSON.stringify(args ?? {})` 每次 invoke 必做(`server.ts:414` 调用);`fs.write` 的 `data` 可达数 MB(协议单帧,ws 默认 maxPayload 隐式放行)。
- **机理**:同步 `JSON.stringify` + sha256 一个 8MB 字符串 ≈ 数十毫秒级 CPU 占用,且**阻塞事件循环** —— 期间 GUI 注入(`input.*`)、事件推送、保活 ping 全部延迟。输入控制类能力对延迟敏感(README 自陈助手预热就是为了省 1-3s)。
- **触发条件**:任何带大 payload 的调用(`fs.write` 大文件、`clip.set` 图片、`key.type` 长文本)。
- **修复方向**:超过阈值(建议 64KB)只对前 64KB 求摘要并记录总长度(`args_digest` 语义从「全量哈希」改为「截断哈希」需在审计文档注明口径——**只影响审计摘要,不影响安全属性**)。单测:大参数不抛、摘要稳定、耗时阈值不设内容判据(守卫铁律:禁时长阈值当判据 → 用「只序列化前 N 字节」的结构断言)。
#### A-P0-5(PRD P0-1 补充证据)selfupdate 的失败信息零诊断,与 sidecar 已验证方案差距明确
- **证据**:`selfupdate.ts:71-77` catch 只包 `url`;无 ECONNRESET/ENOTFOUND/ABORT 分类。对照 `apps/console/sidecar/main.mjs:146-165` 已有 `isAbortLike`/`describeNetError` 成熟实现(注释记载第一版把 ECONNRESET 误报成「超时」的真机教训)。
- **触发条件**:境内/代理环境 `fetch failed`(Node fetch 不读代理环境变量,`sidecar/proxy.mjs:5-12` 有真机取证)。
- 本体修复见 B-P0-1;此条登记为「错误分类必须同款移植,不许重新发明」。
#### A-P0-6(PRD P0-2 补充证据)rollback 的能力圈边界:「完全起不来」时 RPC 通道本身不存在
- **证据**:`index.ts:258-261` 启动失败 `process.exit(1)`;`lkg.ts:22-28` 明确「能启动 = 至少不是打死状态」,反证「打死的版本」根本不会运行 → 没有监听 → CLI 无法 invoke。
- **机理**:`nodeagent rollback` 本质是 RPC 命令,**要求被控端可达**。它覆盖的场景是「新版本能启动但需要回退(行为异常/主动避险)」;「新版本进程完全起不来」只能走 `docs/DELIVERY-PIPELINE.md` 附录 C 的物理/带外恢复(备份文件 + LKG 文件手工还原)。
- **修复方向**:不是代码修复,是**契约澄清** —— rollback 的帮助文本/README/CHANGELOG 必须写明这一边界,不得暗示「失联也能一条命令救回」。是否要做「启动失败 N 次自动恢复 LKG」的看门狗,与 DELIVERY-PIPELINE §6「刻意不做自动回滚」冲突 → 列为待决策点 R-1。
### A.2 P1(5 条新发现)
#### A-P1-1(新)L2 的 collect/enable 把审计写成了 `agent.start` —— 事件类型与事实不符,污染生命周期查询与指标
- **证据**:`apps/agent/src/capabilities/audit-l2.ts:207`(collect)与 `:286`(enable)均 `audit({ type: 'agent.start', status: 'ok', reason: 'l2.…' })`。
- **机理**:`agent.start` 语义是「agent 进程启动」(`index.ts:166` 写入)。L2 动作混入后:`nodeagent audit --type agent`、控制台审计页筛「agent 生命周期」、`computeMetrics` 按 type 的统计都会把「采集了一次 L2」当成「agent 启动过一次」——**观测污染**,且不报错(这正是 C-01 同类漂移)。
- **修复方向**:`packages/protocol/src/audit-types.ts`(唯一真源)新增 `agent.l2` → `apps/agent/src/audit.ts:13-46` 联合类型同步(编译期 AssertExact 自动守卫)→ `apps/console/src/types/index.ts:218-233` 前端联合同步 → `audit-filter.test.mjs` 正反守卫自动覆盖。三处同步是契约三段铁律的落地。
#### A-P1-2(新)monitor 的 tick 无重入保护 —— 慢采样源会并发堆积
- **证据**:`apps/agent/src/capabilities/monitor.ts:235` `timer: setInterval(() => { void tick(rec); }, intervalMs)` —— 前一个 `tick`(内含 `await takeSample`,command 源要 exec 一整条命令)未返回时,下一个 interval 照常触发。
- **触发条件**:`source=command` + 命令耗时 > interval(interval 下限 500ms,`monitor.ts:210`),或多监控并发。
- **后果**:并发 `execCommand` 数量无上限(进程风暴);样本文件 append 交错(`appendFileSync` 同步不会写坏行,但样本时间序失真)。
- **修复方向**:`tick` 加 in-flight 守卫(跳过本轮并记 `skipped` 观测字段);`takeSample` 对 command 源显式给超时(与 `system.shell.exec` 口径一致)。
#### A-P1-3(新)事件 watcher 的 exec 失败全静默 —— 「没有事件」与「采不到」不可区分
- **证据**:`apps/agent/src/events.ts:128`(process watch)与 `:163`(net watch)均 `if (err) return;`。
- **触发条件**:`tasklist`/`ps`/`netstat` 不存在、被杀软拦、超时(exec 无 timeout 选项——用的是 `node:child_process` 的 `exec`,`maxBuffer: 4MB`,**无超时**)。
- **后果**:用户订阅了进程启停,实际采了 0 个样本,界面显示「0 事件」—— 与「真没有进程启停」完全同形。
- **修复方向**:失败时 `push(rec, 'watch_error', …)` 推一条(file watch 已有 `watch_error` 先例,`events.ts:113-115`);给 exec 加 `timeout`。
#### A-P1-4(新)`readAudit` 的 `total` 与 manifest 描述不符 —— 跨轮转文件时偏小
- **证据**:`apps/agent/src/audit.ts:346-372`:`total` 只累加「已读文件」的条数,而 `:368` 凑够 `limit` 即 `break`,后续轮转文件不再读;manifest 描述是「本次读取的总条数(未截断前)」(`packages/protocol/src/capabilities/manifest/system.ts:234`)。
- **后果**:控制台「共 N 条」在记录量大时偏小(展示偏差,不影响安全与功能正确性)。
- **修复方向**(二选一,建议前者):① 把 total 改为「扫描到的候选文件全量条数」(提前退出只影响 entries 不影响 total——需要把 break 后的 total 补算改为近似值并注明);② 改 manifest 描述为「已扫描文件的总条数」。属契约描述与实现对齐,登记 P1 偏轻。
#### A-P1-5(PRD P0-1 设计输入)被控端无任何出网模块,方案 A 是「新设一个被控端子系统」
- **证据**:`apps/agent/src` 无 net/ 目录;`grep httpGet|resolveProxy apps/agent/src` = 0。sidecar 的方案(`apps/console/sidecar/proxy.mjs`,524 行,含 16 用例守卫 `tests/unit/sidecar-proxy.test.mjs`)是 .mjs + JSDoc;被控端是 TS + esbuild 单文件打包。
- **关注点**:① 移植必须保语法但改类型;② 被控端由计划任务/服务拉起时**环境变量不可见**(Windows 计划任务默认不继承用户 shell 的 HTTPS_PROXY)→ 系统代理探测(reg query)才是主力路径,文档必须写;③ agent.mjs 单文件打包,新增模块不要引外部依赖(铁律:零原生编译、被控端零依赖部署)。
- 设计见 B-P0-1。
### A.3 P2(登记,本轮不修)
| # | 项 | 证据 | 说明 |
|---|---|---|---|
| A-P2-1 | `task.ts:117` 死分支 `rec.state = code === 0 ? 'done' : 'done'` | 实读 | 意图应为非零也 done(exit_code 区分)或 failed;现状无害但坏味道 |
| A-P2-2 | `validate.ts:38` 每次调用 `new RegExp(schema.pattern)` | 实读 | 热路径重复编译;可缓存(pattern 来自 manifest,进程内不变) |
| A-P2-3 | ws `maxPayload` 未显式声明 | `server.ts:637` `new WebSocketServer({ server })` | 依赖 ws 默认值(100MiB)——隐式契约;建议显式 16MiB 并在 manifest fs.write 注明 |
| A-P2-4 | `readAudit total` 语义 | 见 A-P1-4 | 若选方案②则降为 P2 |
| A-P2-5 | 大文件拆分 | PRD P2-2 已登记(`mcp/src/index.ts` 530 行、`window.ts` 665 行) | 清单驱动小步拆,纯移动 |
| A-P2-6 | 协议 fuzz / TLS 夹具 / 覆盖率 / 分支保护 / Linux-macOS L2 | PRD P2-3~P2-7 | 全部推后 |
### A.4 未发现新违律的点(留档,避免下一轮重复审)
- Windows 陷阱铁律:`exec.ts:44-69` 数组传参不拼 shell 字符串;`ps-helper.ts:240` `windowsHide: true`;`selfupdate.ts:170-172` 就地覆写不 rename(注释有真机记录);`version-write.mjs` 的 CRLF 纪律完整。
- 守卫铁律:`lkg.test.mjs`、`mcp-tools.test.mjs`(sha 序列)、`console-commands.test.mjs`(invoke⇄handler 双向)、`cli-options.test.mjs`(L-01 总闸)均为「会红」的断言式守卫。
- 错误规范:`server.ts:434-458` 协议级错误 throw / 能力内部 CapabilityError → `{status:'failed'}` 信封,分层正确。
- 审计哈希口径:`audit.ts:107-111` 与 `audit-l2.ts:67-71` 均为「对含 prev 的记录求哈希」,同构。
---
## Part B:增量设计方案(P0/P1 逐项,文件级)
### B-P0-1 被控端自更新走代理出网(方案 A)
#### 模块边界与依赖方向
```
apps/agent/src/net/proxy.ts (新增,~450 行 TS;只依赖 node 内置模块)
▲
│ 单向依赖(selfupdate → proxy)
apps/agent/src/capabilities/selfupdate.ts (改 download())
```
- `net/proxy.ts` **不 import 任何项目内模块**(不碰 config/audit/capabilities)—— 保证可被单测独立加载、可被将来其它出网能力复用;依赖方向严格单向,杜绝环。
- `sidecar/proxy.mjs` → TS 移植映射表:
| proxy.mjs 导出 | proxy.ts 导出(签名) |
|---|---|
| `normalizeProxyUrl(raw)` | `normalizeProxyUrl(raw: unknown): URL \| null` |
| `isBypassed(targetUrl, noProxy)` | 同 |
| `pickEnvProxy(env, targetUrl)` | `pickEnvProxy(env: Record<string,string\|undefined>, targetUrl: string): {url,source,notes}` |
| `parseScutilProxy(text)` / `parseRegQueryProxy(text)` | 同 |
| `detectSystemProxy(platform, run)` | `detectSystemProxy(platform = process.platform, run = runQuiet)`(`run` 可注入假 runner) |
| `resolveProxy({targetUrl, env, system})` | 同 |
| `httpGet(url, {proxy, timeoutMs, maxRedirects})` | 同,返回 `{status,statusText,headers,body,url}` |
| `tunnelConnection(proxy, target, signal)` | 同(CONNECT 隧道 + 自管 abort,保留 proxy.mjs:359-364 的真机教训注释) |
#### `selfupdate.ts` 改动
```ts
// 改:download(url, timeoutMs)
async function download(url: string, timeoutMs: number): Promise<{ buf: Buffer; via: OutboundInfo }> {
const out = await resolveProxy({ targetUrl: url }); // NA_PROXY → env → 系统代理;PAC/SOCKS 记 note 不静默
const res = await httpGet(url, { proxy: out.proxy, timeoutMs });
// 保留:!res.ok → CapabilityError(EXECUTION_FAILED);content-length 预检;流式上限
// 错误分类同款移植 sidecar/main.mjs:146-165(isAbortLike / describeNetError)
}
```
- **超时/取消语义**:`timeoutMs`(现有参数,默认 120s,范围 5s~600s,`selfupdate.ts:85`)作为**整条重定向链的总预算**下发给 `httpGet`(与 sidecar 一致:链共享一个 AbortController,防 5 跳 × 30s 穿过 RPC 超时)。能力一旦启动无外部取消通道(协议无取消方法),超时即断由 AbortController 保证。
- **返回结构(只增)**:`updated/dry_run/bytes/previous/current_hash/incoming_hash/backup_path/restarted` 之外新增 `via`、`proxy`、`proxy_source`、`outbound_notes`;manifest `AgentUpdate.returns_schema`(`manifest/system.ts:423-435`)同步补这四个属性(契约三段:实现 → manifest → 前端类型;console 的 `UpdateInfo` 已有同名字段可对齐)。
- **失败信封**:`CapabilityError.data` 带 `{url, via, proxy, proxy_source, timeout_ms, notes, hint}`——「代理没配 / 哈希不符 / 超时」三类可分(PRD G1 的可诊断验收)。
- **安全属性不变**(逐条对照):仍只 http/https(`selfupdate.ts:100-102` 保留);sha256 仍必填且逐字节校验(`:116`);先校验后落盘(`:126` 之前不写目标);写后校验 + 不一致回滚(`:183-195`);TLS 证书默认校验(`tls.connect` 不传 `rejectUnauthorized:false`,与 proxy.mjs:39 纪律一致)。
- **备份清理(配套 A-P0-3)**:替换成功后,扫入口同目录 `basename(script).bak-*`,按 mtime 保留**最近 3 个**,其余删(删除失败忽略——被杀软占位的 .bak 删不掉不影响正确性)。抽成纯函数 `planBackupCleanup(files: {name,mtime_ms}[], keep=3): string[]` 供单测。
- **agent.json 不加配置项**(NA_PROXY 环境变量 + 系统探测已覆盖;YAGNI)。Windows 计划任务拉起时环境变量不可见的问题写进 DEVELOPMENT.md(系统代理是主力路径)。
#### 单测(新增 `tests/unit/agent-proxy.test.mjs`,≥16 用例,口径对标 sidecar-proxy.test.mjs)
1. `NA_PROXY` 优先于一切;2. env 顺序(https: 列表逐键命中);3. `HTTPS_PROXY` 大小写/裸主机补 `http://`;4. `NO_PROXY` 精确/后缀/`host:port`/`*`;5. socks5 记 note 且 `proxy=null`(不静默直连);6. scutil 解析(HTTPSEnable 优先 + ExceptionsList);7. reg 解析(ProxyEnable=0 → 无代理;分协议形态 https 优先);8. PAC → note;9. `<local>` 剔除;10. CIDR 条目不猜(note);11. `resolveProxy` 系统 bypass 命中直连;12. `httpGet` 重定向链共享预算(假 server 验证 abort 一次断全链);13. CONNECT 非 200 → 明确错误;14. http 目标经代理走绝对形态(假代理服务器断言请求行);15. `planBackupCleanup` 保留最近 3;16. 下载上限中断(content-length 超限 + 流式超限两条)。
#### 兼容性影响
- L0 被控端变更 → 必须发版(v2.1.0 MINOR 成立:新增返回字段 + 行为修复,无破坏)。
- 旧控制端调 `system.agent.update`:不认识新返回字段,忽略即可(VERSIONING §3.3「返回结构只加不减」)。
- 控制台会把被控端播种到同版本(`seed_agent_script`)——v2.1.0 发布时被控端随之播种,自更新链路在发布后即带代理能力;**发布顺序**:先发 v2.1.0(agent 含代理)再让设备自更新,天然兼容(旧版 agent 的 update 能力参数不变)。
### B-P0-2 `nodeagent rollback`(LKG 回滚入口)
#### 语义定义(含能力圈边界,见 A-P0-6)
- 目标版本:**LKG**(`system.info.lkg.lkg.hash`)。`--backup <远端路径>` 为高级逃生门(当前已是 LKG 但需回退到更早备份时用)。
- 前提:被控端**可达**(RPC 命令)。进程完全起不来的场景走 DELIVERY-PIPELINE 附录 C(物理/带外),帮助文本明示。
- **不提供自动回滚**(维持 DELIVERY-PIPELINE §6 取舍,本项只补手动入口)。
#### 数据结构(`apps/agent/src/lkg.ts` 只增两字段)
```ts
export interface LkgRecord {
hash: string; // 前 12 位(既有,展示/比对口径不变)
hash_full?: string; // 新增:sha256 全量 64 位 —— 备份指纹匹配用全值,消除 12 位前缀碰撞的理论风险
bytes: number;
script_path: string;
backup_glob?: string; // 新增:回滚内容候选的文件名模式(`<basename>.bak*`),供控制端枚举
recorded_at: number;
node_version: string;
platform: string;
}
```
`recordLKG()` 写全量哈希;`readLKG()` 对老文件(无 `hash_full`)回落 12 位比对(向后兼容,不猜)。
#### 指纹匹配算法(CLI 侧,只用既有能力,零协议变更)
1. `fs.list` 入口所在目录 → 过滤 `^<basename>\.bak(-.*)?$`(deploy 固定名 + selfupdate 时间戳名)→ 按 `mtime` 新→旧;
2. 对每个候选:`fs.read` 分块(512KB,`encoding: 'base64'`)流式计算 sha256;
3. 与 `lkg.hash_full ?? lkg.hash` 比对(LKG 只有 12 位时按前缀比);
4. 首个命中 = 回滚源;全部落空 → 列出各候选指纹,报错退出(**不动任何文件**)。
#### 执行流程(对齐 deploy 的成熟姿势)
```
① system.info → agent_script / build.hash / lkg
② 无 LKG ───────────────→ 报错退出(exit 2),零改动
③ current == LKG.hash ──→ 「当前即 LKG」提示退出(提示 --backup 逃生门)
④ 枚举备份候选(fs.list + 过滤)
⑤ 指纹匹配(流式读 + sha256)── 无匹配 → 列候选指纹报错退出
⑥ 覆盖写入:fs.write append:false 首块 + append:true 续块(同 cmdDeploy ops.ts:321-334)
写后校验:fs.read 回读算 sha256
├─ 一致 → ⑦
└─ 不一致 → 用「回滚源自身」再覆盖一次(还原到回滚源内容=原状)→ 报错退出(带 backup 路径)
⑦ system.agent.restart(独立连接,吞连接中断,同 ops.ts:355-365)
⑧ probeOnce 轮询直到 build.hash == LKG.hash(90s 墙钟,同 ops.ts:368-389)
```
#### CLI 设计
- 新文件 `apps/cli/src/cmd/rollback.ts`,导出:
- `planRollback(info): {action:'no-lkg'|'already-lkg'|'explicit'|'match', targetHash, scriptPath}`(纯函数)
- `pickBackupByHash(cands, wantHash): {path,hash}|null`(纯函数,mtime 排序在外层)
- `hashRemoteFile(client, path): Promise<{hash,bytes}>`(流式,client 注入 → 可假测)
- `cmdRollback(opts)`(主编排)
- 改 `apps/cli/src/index.ts`:`case 'rollback': await cmdRollback(opts)`;
- 改 `apps/cli/src/options.ts` + `types.ts`:新增 `backup: string` 选项(`dry-run` 已声明于 `options.ts:124`,L-01 总闸自动覆盖);
- 改 `apps/cli/src/help.ts`:命令速查加 `rollback` 行(L-01 守卫:HELP 里每个 `--flag` 必须在 parseArgs 声明——新增 `--backup` 两边同步)。
- 输出三段式(PRD §3.2):LKG 记录(hash/bytes/recorded_at/node_version)→ 指纹比对 → 执行结果与复核提示;`--dry-run` 只做 ①~⑤(含匹配结论),零写入。
- 退出码:0 成功/已是 LKG;1 执行失败(含写后校验失败);2 前提不满足(无 LKG/无匹配备份)。
#### 单测(`tests/unit/rollback.test.mjs`,PRD 验收②三条全覆盖)
1. **无 LKG**:`planRollback` 返回 `no-lkg`,`cmdRollback` 不发起任何 invoke(假 client 断言零调用);
2. **指纹已匹配**:`already-lkg` 分支零写入、退出 0;
3. **写后校验失败自动回滚**:假 client 让第二次 `fs.read`(回读校验)返回被篡改内容 → 断言流程执行了「还原写」且最终 `fs.read` 到的内容 == 回滚源内容,退出码 1;
4. `pickBackupByHash`:mtime 新→早优先、前缀/全量两种比对、空候选返回 null;
5. dry-run:只读 invoke(system.info/fs.list/fs.read),无 fs.write/restart。
#### e2e/真机(PRD 验收③)
- `tests/e2e/run.mjs`:起被控端 → `deploy` 一份新 agent.mjs(或 update dry-run 后手工替换)→ `rollback --dry-run`(只读断言)→ 实滚 → probe 复核 `build.hash == LKG`。端口/临时 HOME 遵守既有纪律。
- 真机矩阵(阶段 4):Windows x64 必跑一遍「更新→回滚→复核」闭环。
### B-P1-1 MCP 新增 `na_audit_l2`(41→42)
- `apps/mcp/src/tools/defs.ts`:在 `na_audit` 之后插入 `na_audit_l2`:
```ts
{
name: 'na_audit_l2',
description: 'L2 系统级审计兜底(仅 Windows 被控端):采集操作系统自己的审计事件(进程创建/登录),' +
'补 L1 结构上记不到的部分(攻击者绕过 agent 或停掉 agent 时 L1 全盲)。' +
'action=status 查策略/enable 开启(需管理员)/collect 采集落盘/verify 校验 L2 链。' +
'⚠️ 信任边界:本地 audit-l2.log 只是归一化副本,防篡改依赖把记录转发出机,不可当作不可篡改证据。',
inputSchema: { type:'object', properties: {
action: { type:'string', enum:['status','enable','collect','verify'], description:'默认 status' },
since: { type:'integer', description:'collect 起始时间(Unix ms),默认 24h 内' },
limit: { type:'integer', description:'collect 条数上限,默认 1000,最大 5000' },
}, additionalProperties: false },
}
```
- `apps/mcp/src/index.ts`:`TOOL_TO_CAPABILITY` 加 `na_audit_l2: CapabilityNames.AuditL2`;`resolveToolCall` 加 case(`action` 默认 `status`,`since/limit` 直传)。
- 非 Windows:被控端抛 `E_UNSUPPORTED_PLATFORM`(`audit-l2.ts:190-198`)→ 既有 invoke 归一逻辑转成 error result → **明确平台错误而非空结果**(PRD 验收②);单测断言错误名。
- `tests/unit/mcp-tools.test.mjs`:41→42 + `BASELINE_SEQ_SHA` 更新(顺序即呈现顺序,插在 `na_audit` 后)。
- **决策记录**:PRD 给了「新工具 vs na_audit 增参」二选一 —— 选**新工具**:工具表只增(旧宿主零副作用)、错误面隔离(平台不支持时不影响 na_audit)、信任边界文案有独立载体。
### B-P1-2 控制台 L2 审计视图
- `apps/console/src/views/AuditView.vue`:既有审计页内**增加 L2 分区**(不新增顶级路由,PRD §3.2):
- 上部 L2 状态卡:`platform/supported/auditpol/legacy_policy_disabled/cmdline_logging/sysmon_installed/file/file_exists`(与 `audit-l2.ts:247-259` 返回逐字段对齐);
- 下部操作行:collect(since/limit 输入 + 按钮)、verify(按钮);结果表列 `ts/kind/subject/object/event_id`(归一化 schema,`docs/AUDIT-L2.md` §4);
- 非 Windows:整区置灰 + 文案(复用既有门控模式)。
- `apps/console/src/stores/conn.ts:17`:`WINDOWS_ONLY_CAPS` 增加 `'system.audit.l2'`(该数组是事前门控真源,README:389-391 的 app./service 同款机制)。
- `apps/console/src/types/index.ts`:新增 `L2Status`/`L2Entry`/`L2VerifyResult`/`L2CollectResult` 类型(与 B-P1-4 补全后的 returns_schema 对齐)。
- **无需新 Tauri 命令**(走既有 `invoke_capability`,`api/index.ts` 泛型通道)→ `console-commands.test.mjs` 双向守卫不受影响,零 Rust 改动。
- 新增 `tests/unit/console-l2.test.mjs`:① AuditView 引用的每个 L2 字段名都能在 manifest `AuditL2.returns_schema` 顶层属性中找到(B-P1-4 落地后此守卫才生效——反向守卫同 console-contract 思路);② `WINDOWS_ONLY_CAPS` 含 `system.audit.l2`;③ 界面纪律源码守卫(不引图标库、只用 `--na-*` 变量——扫描新增代码块)。
### B-P1-4 `system.audit.l2` returns_schema 补全
- `packages/protocol/src/capabilities/manifest/system.ts:296-322`:`returns_schema` 从 `{ type: 'object' }` 补为四种 action 返回的**顶层属性并集**(校验器不支持 oneOf/anyOf,用并集 + description 说明「按 action 出现」,与仓库现有风格一致):
```ts
returns_schema: {
type: 'object',
description: '按 action 返回:status→策略状态;enable→启用结果;collect→采集结果;verify→链校验结果',
properties: {
// status
platform: { type: 'string' }, supported: { type: 'boolean' },
auditpol: { type: 'string' },
legacy_policy_disabled: { type: 'boolean' }, cmdline_logging: { type: 'boolean' },
sysmon_installed: { type: 'boolean' },
file: { type: 'string' }, file_exists: { type: 'boolean' }, note: { type: 'string' },
// enable
enabled: { type: 'boolean' }, policies: { type: 'array', items: { type: 'string' } }, hint: { type: 'string' },
// collect
collected: { type: 'integer' }, written: { type: 'integer' }, since: { type: 'integer' }, limit: { type: 'integer' },
// verify
ok: { type: 'boolean' }, checked: { type: 'integer' },
broken_at: { type: 'object', properties: { line: { type: 'integer' }, reason: { type: 'string' } } },
},
},
```
- 与 `audit-l2.ts` 四个返回形状(`:247-259` / `:287-293` / `:208-216` / `:314-346`)逐字段核对;**运行时返回不变**(纯文档性契约补齐 → 旧控制端零影响,符合 VERSIONING §3.3)。
- `console-contract.test.mjs` 的反向守卫对该能力从「跳过空壳」转为生效;若其实现按名单跳过,需把 `system.audit.l2` 移出豁免名单(或显式记录豁免理由——PRD 验收②的「或」分支)。
### B-P1-5 L2 e2e 覆盖
- `tests/e2e/run.mjs`(跨平台,Linux runner 可跑):`system.audit.l2 status` 在非 Windows 必须抛 `E_UNSUPPORTED_PLATFORM`(明确平台错误,不返回空结果)——1 条。
- `tests/e2e/windows.mjs`(Windows 专属):① status 返回结构可解析(字段齐全);② collect(since/limit)对空 Security 日志不崩、返回 `collected/written/file`;③ collect 后 verify 返回 `ok:true`;④ 自造 L2 链文件篡改后 verify 报 broken_at(可在单测层做,e2e 做 ①~③)——3 条。
- 纪律:不新增端口常量冲突、临时 HOME 隔离遵守既有模式。
### B-P1-6 `na-gui` 陈旧计划任务清理
- 抽纯函数到 `scripts/lib/scheduled-tasks.mjs`:`TASK_WHITELIST = ['nodeagent', 'na-gui', 'nodeagent-console']`(白名单,禁通配删除);`planTaskCleanup(existing: string[]): string[]`(返回需删的名单,只删白名单内且存在的)。
- `scripts/uninstall.ps1`:卸载流程末尾对白名单逐个 `Unregister-ScheduledTask`(存在性检查 + 动作进日志;纯 ASCII + CRLF + BOM 纪律,参照 `tests/unit/ps1-encoding.test.mjs` 守卫)。
- `scripts/control.ps1`:新增 `cleanup` 子命令(复用同一函数块),供「拿到端口占用提示后自救」。
- 验证:`tests/unit/na-gui-cleanup.test.mjs` 测纯函数(建名单→计划删除→断言只删白名单);ps1 侧加源码守卫(uninstall.ps1 必须包含白名单清理调用与日志)。真机验证(阶段 4):Windows 上建同名任务→跑 uninstall→断言已删。
### B-P1-3 文档回填(CHANGELOG / README / DEVELOPMENT / ROADMAP + 数字守卫)
- **CHANGELOG.md**:`[未发布]` 段重组 —— 当前内容(控制台去命令行化)归位到 `v2.0.3` 已发布节之下方?不:`[未发布]` 现有主题是**未归位**的控制台批次。按 PRD 验收②:`git log v2.0.3..HEAD` 全部提交主题进未发布段,分节:L2 系统级审计(`7d781d1`)、交付流水线(`2bf45fd`)、Linux 适配(`00992e5`)、质量门禁(`141f479`)、console-release(`9f95560`)、console 修复批次(`240c52c/0782d90/9af6e0f/0065916/704fb8c/3680218`)+ 本轮 v2.1.0 新增节(selfupdate 代理 / rollback / MCP L2 / 控制台 L2 / na-gui 清理 / L2 returns_schema+e2e / 文档数字)。
- **README.md**:`51 项能力 · 41 个 MCP 工具 · 269 单测 · 30 e2e · 3 Rust`(:171)→ `52 · 42 · 357 · 33+13 · 18`;MCP 工具表加 `na_audit_l2`;命令速查「自持与运维」加 `nodeagent rollback [--dry-run]`;「控制台安装包只作为 CI artifact」行(:387)已被 `9f95560` 修掉 → 删。
- **docs/DEVELOPMENT.md**:补 L2 能力条目(params/returns 落定后契约)、selfupdate 代理出网机制(NA_PROXY→env→系统代理 + 计划任务环境变量不可见的告诫)、rollback 能力圈边界。
- **docs/ROADMAP.md**:台账刷新(本轮 P0/P1 标记完成)。
- **数字守卫(把 PRD 验收③的「或至少人工核对」升级为断言)**:新增 `tests/unit/docs-numbers.test.mjs`:manifest 实点数 vs README「52 项能力」、defs.ts 工具名数 vs README「42」、`grep -c "test("`? 不——单测数不扫源码(会脆),改为:README 数字段与 `docs/ARCH/PRD 基线表` 的一致性由**人工**登记 + 守卫只覆盖「能力数/工具数」两个可从代码数出的值(铁律:守卫要能注入故障变红——改动 manifest 增能力不改 README → 红)。
---
## Part C:任务列表(工程师执行顺序)
> 铁律遵守:≤5 任务;每任务 ≥3 文件;T01 = 基础设施(版本/配置/入口);被控端(L0)只增不改既有行为;每个 P0/P1 完工即补「会红的守卫」。
### T01 项目基础设施:版本号统一写入 v2.1.0 + CHANGELOG 骨架
- **Source Files**:
- `scripts/lib/version-write.mjs`(不修改,作为写入工具被调用)
- 版本落点(一律经 version-write 统一写,禁手改散点):根 `package.json`、各 workspace `package.json`、`apps/console/src-tauri/tauri.conf.json`、`Cargo.toml`、`Cargo.lock`
- `CHANGELOG.md`(`[未发布]` 段重组为 v2.1.0 骨架)
- `tests/unit/version-write.test.mjs`(若钉了具体版本号则同步)
- **Dependencies**:无
- **Priority**:P0
- **验收标准**:① `node scripts/lib/version-write.mjs` 写入后 `git diff --stat` 覆盖全部 11 处且无散点遗漏;② `pnpm build` 后 `system.info` 自报 v2.1.0;③ CHANGELOG 未发布段涵盖 `git log v2.0.3..HEAD` 全部主题;④ 工作区干净后才允许进入发版流程(铁律 7)。
- **预估改动量**:~100 行(CHANGELOG 重组为主)
### T02 被控端 L0:selfupdate 代理出网(P0-1)+ L2 returns_schema(P1-4)+ 备份清理(A-P0-3)
- **Source Files**:
- 新增 `apps/agent/src/net/proxy.ts`
- 新增 `apps/agent/src/net/index.ts`(桶文件,导出面收敛)
- 改 `apps/agent/src/capabilities/selfupdate.ts`(download 走代理 + 诊断字段 + 备份清理 + 错误分类)
- 改 `packages/protocol/src/capabilities/manifest/system.ts`(AuditL2 returns_schema 补全;AgentUpdate returns 加 via/proxy/proxy_source/outbound_notes)
- 新增 `tests/unit/agent-proxy.test.mjs`(≥16 用例)
- 新增 `tests/unit/selfupdate-backup-cleanup.test.mjs`(planBackupCleanup + 下载上限,可并入 agent-proxy)
- **Dependencies**:T01(版本随包)
- **Priority**:P0
- **验收标准**:① 单测 16+ 全绿(含 NO_PROXY/socks 报错/PAC note/重定向共享预算);② `system.agent.update` 带 `--dry-run` 在配代理的离线环境(本地 HTTPS 源站 + 本地代理)端到端成功;③ 哈希不符/代理不可达时不落盘且错误带 via/hint;④ 357 单测 + e2e 33 全绿回归;⑤ manifest 变更后 `capabilities.test.mjs`/`console-contract.test.mjs` 守卫全绿。
- **预估改动量**:~700 行(proxy.ts 450 + selfupdate 120 + manifest 40 + 测试 200)
### T03 控制端 L1:`nodeagent rollback`(P0-2)
- **Source Files**:
- 新增 `apps/cli/src/cmd/rollback.ts`
- 改 `apps/cli/src/index.ts`(注册 case)、`apps/cli/src/options.ts`(`backup` 选项)、`apps/cli/src/types.ts`(Options 字段)、`apps/cli/src/help.ts`(用法 + 速查行)
- 改 `apps/agent/src/lkg.ts`(`hash_full`/`backup_glob` 只增字段,老记录回落 12 位)
- 改 `tests/unit/lkg.test.mjs`(补 hash_full 用例)
- 新增 `tests/unit/rollback.test.mjs`(5+ 用例,含写后校验失败回滚)
- **Dependencies**:T01(独立于 T02,可并行)
- **Priority**:P0
- **验收标准**:① `--dry-run` 只读(零写 invoke);② 单测覆盖无 LKG/指纹已匹配/写后校验失败自动回滚;③ e2e 走「更新→回滚→复核」闭环(T05 补 e2e 时联调);④ L-01 总闸(help flag ⇄ parseArgs)全绿;⑤ README/HELP 文案写明能力圈边界(可达性前提)。
- **预估改动量**:~450 行(rollback.ts 250 + 测试 150 + 周边 50)
### T04 三端暴露 L2:MCP `na_audit_l2`(P1-1)+ 控制台 L2 视图(P1-2)+ L2 审计类型修正(A-P1-1)
- **Source Files**:
- 改 `apps/mcp/src/tools/defs.ts`(na_audit_l2)、`apps/mcp/src/index.ts`(映射 + 分发)
- 改 `tests/unit/mcp-tools.test.mjs`(41→42 + BASELINE_SEQ_SHA)
- 改 `apps/console/src/views/AuditView.vue`(L2 分区)、`apps/console/src/stores/conn.ts`(WINDOWS_ONLY_CAPS +system.audit.l2)、`apps/console/src/types/index.ts`(L2 类型)
- 改 `packages/protocol/src/audit-types.ts` + `apps/agent/src/audit.ts` + `apps/console/src/types/index.ts`(新增 `agent.l2` 类型三处同步);改 `apps/agent/src/capabilities/audit-l2.ts`(collect/enable 审计改 `agent.l2`)
- 新增 `tests/unit/console-l2.test.mjs`(字段名 ⇄ returns_schema 反向守卫 + 门控 + 界面纪律)
- **Dependencies**:T02(manifest returns_schema 先落,前端守卫才有比对真源);与 T03 并行
- **Priority**:P1
- **验收标准**:① MCP 工具表 42、序列 sha 更新、`na_audit_l2` 在非 Windows 返回明确平台错误(单测断言错误名);② 控制台 L2 区在 macOS 被控端置灰;③ `agent.l2` 类型三处同步后 `audit-filter.test.mjs` 自动覆盖;④ console-commands 双向守卫不回归。
- **预估改动量**:~500 行(前端 250 + MCP 80 + 类型同步 50 + 测试 120)
### T05 收口:L2 e2e(P1-5)+ na-gui 清理(P1-6)+ 文档回填与数字守卫(P1-3)
- **Source Files**:
- 改 `tests/e2e/run.mjs`(L2 平台错误用例)、`tests/e2e/windows.mjs`(L2 status/collect/verify 3 条)
- 新增 `scripts/lib/scheduled-tasks.mjs`(纯函数);改 `scripts/uninstall.ps1`、`scripts/control.ps1`(白名单清理 + cleanup 子命令)
- 新增 `tests/unit/na-gui-cleanup.test.mjs` + `tests/unit/docs-numbers.test.mjs`
- 改 `CHANGELOG.md`(v2.1.0 节定稿)、`README.md`(数字/工具表/速查)、`docs/DEVELOPMENT.md`(L2 + 代理 + rollback 边界)、`docs/ROADMAP.md`
- **Dependencies**:T02、T03、T04
- **Priority**:P1
- **验收标准**:① e2e 新增 4 条全绿(33+13 → 37+13 口径);② na-gui 清理只删白名单(单测 + 真机各一遍);③ README/DEVELOPMENT/CHANGELOG 数字与实测一致且 `docs-numbers.test.mjs` 能对「增能力不改 README」变红;④ 全量 357+ 单测 / e2e / Rust 18 回归。
- **预估改动量**:~600 行(e2e 150 + 脚本 80 + 测试 120 + 文档 250)
### 任务依赖图
```mermaid
graph LR
T01[T01 基础设施<br/>版本 v2.1.0 + CHANGELOG 骨架]
T02[T02 被控端 L0<br/>selfupdate 代理 + L2 returns_schema + 备份清理]
T03[T03 控制端 L1<br/>nodeagent rollback]
T04[T04 三端暴露 L2<br/>MCP + 控制台 + agent.l2 类型]
T05[T05 收口<br/>L2 e2e + na-gui 清理 + 文档回填]
T01 --> T02
T01 --> T03
T02 --> T04
T01 --> T04
T02 --> T05
T03 --> T05
T04 --> T05
```
---
## Part D:多端适配差距分析
### D.1 能力矩阵现状(证据:各能力平台分支实读)
| 能力/组 | Windows | macOS | Linux | 证据 |
|---|---|---|---|---|
| system.info/status/process.list/shell.exec/task.*/metrics/audit.*(L1) | ✅ | ✅ | ✅ | 无平台分支 |
| system.service.list | ✅ | ❌ E_UNSUPPORTED_PLATFORM | ❌ 同 | `system.ts:379-386` |
| system.audit.l2 | ✅(auditpol 已落地) | ❌ 明确报错(ESF entitlement 硬限) | ❌ 明确报错(auditd 需预装) | `audit-l2.ts:190-198` |
| app.list / app.install | ✅(注册表 + winget) | ❌ | ❌ | `app.ts:14` |
| screen.info / screen.capture | ✅ | ✅(需屏幕录制权限) | ⚠️ 依桌面环境 | `screen.ts:268`(macOS 权限提示) |
| screen.record | ✅(帧序列 + ffmpeg 可选) | ❌ | ❌ | `record.ts:95` |
| screen.find | ✅ UIA→OCR→图像模板 | ✅ Vision OCR(无 UIA,`where` 明确报错) | ❌ | `window.ts:312`、`darwin.ts:18-19`、README:128-129 |
| window.list / window.focus | ✅ | ✅ AppleScript(需辅助功能权限) | ❌ | `window.ts:154-190`、`darwin.ts:14-17` |
| input.*(键鼠) | ✅(默认禁) | ❌ **只读** | ❌ | `input.ts:39` |
| clip.get/set(文本) | ✅ | ✅ | ✅ | 无分支 |
| clip 图片 | ✅ | ❌ | ❌ | `clipboard.ts:176` |
| event.watch(file/process/net) | ✅ | ✅ | ✅ | `events.ts:124-165` 平台分流 |
| gui.await | ✅ | ✅(复用 window/screen 的平台面) | 部分(file/process 可,window/control 随平台) | `await.ts` 复用 |
| log.query / monitor.* | ✅ | ✅ | ✅ | 无分支 |
| fs.* / audio.* | ✅ | ✅ | fs ✅ / audio ❌(仅 Win+macOS) | `audio.ts:176,209` |
| net.apply/confirm/status | ✅ | ❌ | ❌ | `network.ts:308` |
| system.agent.update/restart | ✅ | ✅ | ✅ | 无分支 |
### D.2 差距结论与处置
1. **本轮补(P1 范围内)**:
- L2 的**暴露面**补齐(MCP 工具 + 控制台视图 + returns_schema)—— Windows 已实现,补的是「三端可见性」,不改平台实现面。
- 非 Windows 的 L2 入口**事前禁用 + 明确原因**(控制台 WINDOWS_ONLY_CAPS 机制;MCP 错误名可断言)。
2. **登记 P2(推后)**:
- Linux L2(auditd 需预装,属部署前置条件, AUDIT-L2.md §3.2 已论证「不写假装能用的代码」);
- macOS L2(ESF entitlement 不可自签启用);
- macOS `input.*` 写入(当前只读;需 AX 授权 + 实现成本,PRD §2.2 诉求⑤已列后续);
- macOS/Linux `screen.record`、`net.*`、图片剪贴板;
- Windows arm64 CI runner(GitHub 不提供,人工真机流程化)。
3. **本轮顺带修正的文档漂移**:README「控制台安装包仅 CI artifact」已被 `9f95560` 修复(P1-3 回填时删该行);README:171 数字全量过期(P1-3)。
---
## Part E:设计风险与需再决策点
| # | 风险/决策 | 影响 | 建议 |
|---|---|---|---|
| R-1 | **rollback 能力圈边界**:被控端「进程完全起不来」时 RPC 通道不存在,CLI rollback 只能救「能启动但需回退」。PRD G1 用户故事表述为「更新后失联→一条命令恢复」 | 若预期「失联也能远程救」,需追加「启动失败 N 次由看门狗恢复 LKG」——与 DELIVERY-PIPELINE §6「刻意不做自动回滚」直接冲突,且会重新引入回滚循环风险 | 维持手动 rollback + 文档明示边界;物理/带外路径(附录 C)不变。是否加看门狗请需求方明确否决或排期 |
| R-2 | **LKG 匹配精度**:LkgRecord 现只有 12 位 hash,备份匹配按前缀比(碰撞概率 1/16¹²,可接受但非零) | 备份指纹匹配的正确性 | 已在 B-P0-2 设计 `hash_full` 只增字段;老记录回落 12 位。零风险落地 |
| R-3 | **MCP L2 形态**:PRD 给「新工具 / na_audit 增参」二选一 | 已代选新工具(工具表只增、错误隔离、文案载体) | 若倾向合并进 na_audit,返工量 ~1 小时(T04 内) |
| R-4 | **计划任务环境变量不可见**:Windows 计划任务拉起的 agent 读不到用户 shell 的 HTTPS_PROXY | 方案 A 的主力路径实际是「系统代理探测」(reg query WinINET),环境变量只在交互 shell 手工启动时生效 | 文档明示;不在 agent.json 加代理配置项(YAGLI/YAGNI)。若要求配置项,+0.5 天 |
| R-5 | **selfupdate 备份保留数**(设计取 3) | 保留太少 → 回退链断;太多 → 磁盘堆积(A-P0-3) | 3 个 + 可被将来配置化;真机验证后定稿 |
| R-6 | **`agent.l2` 新审计类型**:新增即改「审计类型唯一真源」三处 | 漏一处即漂移(C-01 教训) | 编译期 AssertExact + audit-filter 双向守卫兜底;T04 内必须三处同步 |
---
## 附:关键调用流
### selfupdate 代理出网(B-P0-1)
```mermaid
sequenceDiagram
participant CLI as 控制端 (CLI/MCP/console)
participant S as agent server.ts
participant SU as selfupdate.ts
participant PX as net/proxy.ts
participant NET as node:http/https
CLI->>S: invoke system.agent.update {url, sha256, timeout_ms}
S->>SU: agentUpdate(args)
SU->>PX: resolveProxy({targetUrl: url})
PX->>PX: NA_PROXY → env → 系统代理(reg/scutil)
PX-->>SU: {proxy, source, notes}
SU->>PX: httpGet(url, {proxy, timeoutMs})
PX->>NET: CONNECT 隧道 / 绝对形态 / 直连
NET-->>PX: 响应字节流(链共享 AbortController)
PX-->>SU: {status, body}
SU->>SU: sha256 校验(不符→丢弃,不落盘)
SU->>SU: 备份 → 就地覆写 → 写后校验(不符→回滚)
SU->>S: agentRestart({delay_ms:1500})
SU-->>S: {updated, via, proxy, proxy_source, incoming_hash, ...}
S-->>CLI: {status:'ok', data}
```
### rollback(B-P0-2)
```mermaid
sequenceDiagram
participant CLI as cmdRollback
participant C as NodeAgentClient
participant AG as 被控端
CLI->>C: invoke system.info {}
C->>AG: system.info
AG-->>CLI: {agent_script, build.hash, lkg:{lkg, matches}}
alt 无 LKG
CLI-->>CLI: 报错退出(2),零改动
else current == LKG.hash
CLI-->>CLI: 「当前即 LKG」退出(0),提示 --backup
end
CLI->>C: invoke fs.list {path: 入口目录}
C->>AG: fs.list
AG-->>CLI: 候选 *.bak / *.bak-*
loop 每个候选(mtime 新→早)
CLI->>C: invoke fs.read {path, encoding:'base64', offset} 分块
C->>AG: fs.read
AG-->>CLI: chunk
CLI->>CLI: sha256 累加
end
alt 无匹配
CLI-->>CLI: 列候选指纹报错退出(2)
end
opt --dry-run
CLI-->>CLI: 输出比对结论,零写入
end
CLI->>C: invoke fs.write {path: 入口, append:false/true} 覆盖
C->>AG: fs.write
AG-->>CLI: ok
CLI->>C: invoke fs.read 回读校验
alt 校验失败
CLI->>C: invoke fs.write 用回滚源还原
CLI-->>CLI: 报错退出(1)
end
CLI->>C: invoke system.agent.restart(独立连接,吞中断)
loop probeOnce 轮询(90s 墙钟)
CLI->>AG: 握手 + system.info
AG-->>CLI: build.hash
end
CLI-->>CLI: 复核 build.hash == LKG.hash → 成功
```
@@ -0,0 +1,194 @@
# 增量 PRD:nodeagent「全面完善」
> **文档性质**:增量 PRD(简单规格,不做竞品分析)。基线:`3680218`(v2.0.3)。
> 撰写:许清楚(产品经理)|日期:2026-10-25|状态:待需求方确认范围后进入阶段 2。
## 1. 项目信息
| 项 | 值 |
|---|---|
| Language | 简体中文 |
| Programming Language | TypeScript(pnpm monorepo)+ Rust(Tauri 2 壳) |
| Project Name | nodeagent |
| 原始需求复述 | 「请帮我全面完善这个项目:审查现有代码与架构,指出存在的问题和改进空间;补充缺失的核心功能与边界情况处理;优化代码结构、性能与可维护性;完善必要的文档、注释与测试用例;增加多端适配;并给出清晰、可落地的修改方案和代码示例。」 |
### 1.1 基线实测证据(2026-10-25,HEAD `3680218`)
| 项 | 实测值 | 取证方式 |
|---|---|---|
| 单元测试 | **357 全绿** | `pnpm test:unit` 实跑 |
| 端到端 | e2e **33**(`tests/e2e/run.mjs`)+ Windows 专属 **13**(`windows.mjs`) | 用例计数 |
| Rust 单测 | 18 | CHANGELOG 记载 |
| 能力数 | **52**(51 + `system.audit.l2`) | manifest 实点 |
| MCP 工具 | 41 | `apps/mcp/src/tools/defs.ts` 计数 |
| 全量覆盖率 | **66.9%**(6399/9560 行) | `docs/DELIVERY-PIPELINE.md` §8 实测记载 |
| 覆盖率门禁 | **已落地**:变更文件行覆盖 ≥70% 阻断 | `scripts/ci/check-coverage.mjs` + `quality-gate.yml` |
### 1.2 对「历史待改进项」的核实结论(先纠偏,再规划)
主理人转来的 6 条待改进项来自历史审计,其中 **3 条已被近期提交修复或部分修复**。本 PRD 按仓库现状重新基线化,避免把已完工项排进本轮:
| # | 历史待改进项 | 核实结论 | 证据 |
|---|---|---|---|
| 1 | L2 系统级审计兜底未落地,docs/AUDIT-L2.md 暴露 L2 参数是 P1 风险 | ❌ **已落地**。Windows auditpol 方案已实现并发版待发;文档现为已实现功能的设计说明,「未落地暴露」风险消除。Linux/macOS 仍仅设计(macOS 受 Apple entitlement 硬限) | 提交 `7d781d1`;`apps/agent/src/capabilities/audit-l2.ts`;`tests/unit/audit-l2.test.mjs`;manifest `system.audit.l2` |
| 2 | 被控端自更新 `selfupdate.ts` 裸 fetch,境内失败 | ✅ **确认存在,未修** | `apps/agent/src/capabilities/selfupdate.ts:42` 裸 `fetch(url,…)`;CHANGELOG「检查更新在境内必然失败」节自陈「本次未改」 |
| 3 | 陈旧计划任务 `na-gui` 抢占 8765 | ✅ **确认遗留,未清理** | `scripts/uninstall.ps1` 无清理逻辑;`apps/console/src-tauri/src/agent.rs:1084` 测试注释引为动机;控制台侧只做了单实例保护+占用报错提示 |
| 4 | 契约漂移 6 项(缺 seq/session_id/risk 等字段) | ⚠️ **仓库内未找到该台账**,无法确认。已发现的相关漂移见需求池 P1-6/待确认问题 Q3 | 全仓 grep `session_id`/`seq:`/`risk` 于 protocol 源 0 命中;`tests/unit/console-contract.test.mjs` 20 用例守卫在役 |
| 5 | 控制台未接 release.mjs、无自更新通道 | ❌ **接入 Release 已完成**;「控制台自身自更新」仍是真缺口(派生项) | 提交 `9f95560`;`.github/workflows/console-release.yml`(tag 触发,publish job 挂 4 个安装包);`latest.json` 仅含 agent 资产(`scripts/release.mjs` 无 console 字样) |
| 6 | 覆盖率 66.9%,变更文件门禁 ≥70% | ✅ **门禁已落地**(含故障注入验证);66.9% 为存量基线,非缺陷 | `docs/DELIVERY-PIPELINE.md` §2/§8;`quality-gate.yml` |
## 2. 产品定义
### 2.1 产品目标(本次「全面完善」,可度量)
| # | 目标 | 衡量标准 |
|---|---|---|
| G1 | **两条「自我修复」通道恢复可用**:被控端拉取式自更新(境内/代理环境)与更新失败后的 LKG 回滚 | 自更新在配置代理后可拉取成功、失败时报真实原因(带 via/proxy 诊断字段);`nodeagent rollback` 可从 LKG 恢复并复核上线 |
| G2 | **三端暴露完整一致**:52 项能力在 agent / CLI / MCP 三端无缺口,文档数字与实现一致 | MCP 补齐 L2 工具(41→42);README/CHANGELOG/DEVELOPMENT 的能力数、测试数与实测一致;CHANGELOG 覆盖全部未发布提交 |
| G3 | **契约与测试守住新增面**:新能力有契约、有测试、有文档三件套 | `system.audit.l2` 的 `returns_schema` 补齐(消除空壳);L2 有 e2e 覆盖(当前 0);变更文件覆盖率门禁继续有效(≥70%) |
### 2.2 范围界定(关键:6 条诉求 → 做 / 不做 / 后续)
> nodeagent 是**已上线框架**,agent 与 CLI/MCP 分开更新、新旧混用是常态(`docs/VERSIONING.md`)。
> 任何改动按「影响面」分三层,L0 层一律遵守兼容性四铁律(只增不改 / 不加必填参数 / 返回结构只加不减 / 能力集合为唯一事实源)。
**影响面分层**
| 层 | 范围 | 兼容性成本 | 版本动作 |
|---|---|---|---|
| L0 | 被控端 `apps/agent/**`(agent.mjs 运行时) | 高——旧控制端可能仍在跑 | 被控端变更必须 `pnpm release patch|minor`,否则 agent 谎报版本 |
| L1 | 控制端 CLI / MCP / 控制台(`apps/cli`、`apps/mcp`、`apps/console`) | 低——只增工具/命令/页面,旧端忽略新能力 | 随发版走;不触发 agent 版本 |
| L2 | 文档 / 测试 / CI / 脚本 | 无 | 随时可改 |
**诉求映射**
| 用户诉求 | 本次做 | 本次不做 | 后续版本做 |
|---|---|---|---|
| ① 审查代码与架构、指出问题 | 按本 PRD 需求池做**靶向审查**(阶段 2 架构师主导),产出缺陷台账并逐条转守卫 | 全仓重写、架构重构、换框架 | — |
| ② 补充缺失核心功能与边界情况 | selfupdate 代理出网(L0);LKG 回滚入口(L1);MCP L2 工具(L1);控制台 L2 视图(L1);`na-gui` 清理(L2 脚本) | Linux auditd 落地(需预装,属部署前置);macOS ESF(Apple entitlement 不可自签启用);Sysmon 自动安装(第三方驱动) | Linux/macOS L2、Sysmon 并用增强 |
| ③ 优化代码结构、性能与可维护性 | 清单驱动的小步拆分(MCP index/defs、agent 大文件);单一真源收敛;无行为变化 | 大爆炸重构;为拆而拆 | 按坏味道数据持续治理 |
| ④ 完善文档、注释与测试 | CHANGELOG 回填未发布提交;README/DEVELOPMENT 数字与 L2 补章;L2 e2e;`returns_schema` 补全 | 重写历史文档 | 协议模糊测试(fuzz) |
| ⑤ 增加多端适配 | 控制台 L2 页面(Windows 门控,复用既有 `WINDOWS_ONLY_CAPS` 机制);Linux 部署文档现状同步 | macOS 被控端 GUI 写入能力(现为只读);Windows arm64 CI runner(GitHub 不提供,只能人工) | arm64 真机验证流程化 |
| ⑥ 修改方案和代码示例 | 阶段 2/3 产出,PRD 只定范围、优先级与验收 | — | — |
### 2.3 用户故事
1. **被控端运维者**:我在境内有一台无人值守的 Windows 被控端。「我想让它拉取自更新时能走系统代理,并且更新失败时能看到真实原因(是代理没配、还是哈希不符),而不是一句 `fetch failed`。」另外,「万一更新后失联,我希望能有一条命令把它恢复到上一个已知能启动的版本,而不是只能物理到场。」
2. **控制端使用者**:我在控制台审计页除了 L1(agent 自己记的)还想看到 **L2 系统级审计**(进程创建/登录事件)的状态与采集结果,「这样攻击者绕过 agent 直接操作机器时我也留有证据」;非 Windows 被控端该入口应事前禁用并说明原因。
3. **MCP / AI 接入方**:我通过 MCP 接入 nodeagent,「希望 52 项能力全部有对应工具——特别是 L2 审计兜底,我不想为了一个能力再去装 CLI」。
4. **项目维护者**:「每次发版前,README 的能力数/测试数、CHANGELOG 的未发布段必须与实现一致;每个新能力都要有契约(returns_schema)、有测试、有文档三件套,否则我无法相信门禁。」
## 3. 技术规范
### 3.1 需求池
> 每条含:问题描述 / 现状证据 / 期望行为 / 验收标准 / 预估影响面。
> **P0 = 安全或正确性缺陷,必修;P1 = 重要缺口;P2 = 优化项。**
#### P0(必修)
**P0-1 被控端自更新在境内必然失败(裸 fetch 不读代理)**
- 问题:被控端唯一的无人值守修复通道在无透明代理的网络下不可用,且失败信息不含真实原因。
- 证据:`apps/agent/src/capabilities/selfupdate.ts:42` `fetch(url, { signal, redirect:'follow' })`;CHANGELOG「检查更新在境内必然失败」节记录三条根因(Node fetch 不读代理环境变量 / 读不到系统代理 / 超时过短)并自陈「本次未改,属被控端变更」。
- 期望行为:复用 `apps/console/sidecar/proxy.mjs` 已验证的出网方案(`NA_PROXY` → 环境变量 → 系统代理,`NO_PROXY` 命中直连;只支持 HTTP 代理,PAC/socks 明确提示不静默降级),在 agent 侧落地等价能力;超时成对下发;失败信息带 via/proxy/hint。**不削弱任何既有安全属性**(sha256 强制校验、先校验再落盘、写后校验、备份回滚全部保留)。
- 验收标准:① 有代理可达时自更新端到端成功(真机或离线本地代理服务器 + 本地 HTTPS 源站);② 代理不可达时报明确错误且不落盘;③ 新增单测覆盖来源优先级、NO_PROXY、错误映射(参照 sidecar-proxy.test.mjs 的 16 用例口径);④ 回归 357 单测 + e2e 全绿。
- 影响面:L0 被控端(PATCH 起,视范围可并入 MINOR)。
**P0-2 LKG 已落地但控制端无回滚入口(恢复路径断裂)**
- 问题:`docs/DELIVERY-PIPELINE.md` §6 明确「LKG 提供确定的回滚目标,决策权留给控制端(`nodeagent rollback` 读 LKG → 覆盖回入口 → 重启)」,但 CLI 不存在 `rollback` 命令(`apps/cli/src/` grep `rollback` 仅命中 net.ts 的类型注释)。「发布即失联」场景下,设计好的最后一环没有工具兑现。
- 证据:`apps/agent/src/lkg.ts` + `system.info.lkg` + `tests/unit/lkg.test.mjs`(8 例)已在役;CLI 无入口;`docs/DELIVERY-PIPELINE.md` §6 的承诺与实现脱节。
- 期望行为:`nodeagent rollback`:读 LKG → 与当前 agent 指纹比对 → 用 LKG 指向的备份覆盖入口(沿用 deploy/selfupdate 的「就地覆写 + 写后校验」既定姿势)→ 受控重启 → 复核 `system.info.build.hash` 等于 LKG 记录。**不提供自动回滚**(DELIVERY-PIPELINE §6 已论证:无法归因、回滚循环、失控——本项只补手动入口,不改该取舍)。
- 验收标准:① 命令存在且有 `--dry-run`(只读比对不改动);② 单测覆盖「无 LKG / 指纹已匹配 / 覆盖后校验失败回滚」;③ e2e 或真机走一次「更新→回滚→复核」闭环;④ 帮助文本与 README 同步。
- 影响面:L1 控制端(新增命令,只增不改)。
#### P1(重要缺口)
**P1-1 MCP 未暴露 `system.audit.l2`(三端暴露不完整)**
- 问题:L2 已随 agent 落地、CLI 已有 `nodeagent audit l2`(`apps/cli/src/cmd/system.ts:236`),但 MCP 41 个工具中无 L2(`apps/mcp/src/` grep `l2` 0 命中)——AI 接入方完全看不到 L2 能力,与「三端完整」目标相悖。
- 证据:`apps/mcp/src/tools/defs.ts`(41 工具,`na_audit` 仅覆盖 L1 查询);`resolveToolCall` 分发结构。
- 期望行为:新增 `na_audit_l2` 工具(action=status/enable/collect/verify + since/limit),或在 `na_audit` 上增 `l2` 参数面——二选一,需与 MCP 工具表守卫(`tests/unit/mcp-tools.test.mjs` 工具名序列断言)同步。
- 验收标准:① 工具定义 + 分发 + 单测(工具名序列守卫同步更新);② `na_audit_l2` 在非 Windows 被控端返回明确平台错误而非空结果;③ 旧宿主不感知新工具无副作用(MCP 工具表只增)。
- 影响面:L1(MINOR:新增工具)。
**P1-2 控制台无 L2 视图**
- 问题:控制台审计页只有 L1 查询/校验/导出,L2 状态与采集结果无界面入口。
- 证据:`apps/console/src` grep `l2` 0 命中;审计页见 README「审计」行。
- 期望行为:审计页增加 L2 区(status 面板:策略是否启用、两个关键开关 `SCENoApplyLegacyAuditPolicy` / `ProcessCreationIncludeCmdLine_Enabled` 的提示;collect/verify 操作与结果);沿用既有平台门控机制(`conn.ts` 的 `WINDOWS_ONLY_CAPS` 同款思路,非 Windows 事前禁用并说明)。
- 验收标准:① 前端 `invoke` 新命令经 Rust 注册(契约守卫 `console-commands.test.mjs` 自动覆盖 invoke⇄handler、emit⇄listen 双向一致);② 界面在 macOS 被控端下禁用 L2 入口;③ 不加图标、配色走 `--na-*` 变量(遵守既定界面纪律)。
- 影响面:L1(MINOR:新页面元素,协议零变更)。
**P1-3 文档与实现脱节(CHANGELOG 落后约 10 个提交、README 数字过期、DEVELOPMENT 无 L2)**
- 问题:README 仍写「51 项能力 · 269 单测 · 30 e2e · 3 项 Rust 单测」(实际 52 / 357 / 33+13 / 18);CHANGELOG「未发布」段未覆盖 `7d781d1`(L2)、`2bf45fd`(交付流水线)、`00992e5`(Linux 适配)、`141f479`(质量门禁)、`9f95560`(console-release)及 6 个 console 修复;`docs/DEVELOPMENT.md`、`docs/REMOTE-LIMITS.md` 无 L2 章节。
- 证据:上述各文件实测。
- 期望行为:CHANGELOG 回填(L2 / 流水线 / 门禁 / console-release / console 修复批次各一节,注明版本归属);README 数字同步;DEVELOPMENT 补 L2 能力条目与 returns_schema 落定后的契约;ROADMAP 台账刷新。
- 验收标准:① 文档中能力数/单测数/e2e 数与 `pnpm test:unit`、manifest 实测一致;② CHANGELOG 未发布段涵盖 `git log v2.0.3..HEAD` 全部提交的主题;③ 数字一致性有守卫(参照 version-write.test.mjs 的「仓库版本号一致性」思路扩展到能力数/测试数,或至少人工核对记录在案)。
- 影响面:L2(无兼容性成本)。
**P1-4 `system.audit.l2` 的 returns_schema 是空壳(契约不完整)**
- 问题:manifest 中该能力 `returns_schema: { type: 'object' }`,无属性枚举——第三方接入方与前端无契约可依,且 `console-contract.test.mjs` 的反向守卫按设计跳过空壳能力,「契约加了字段前端没跟上」这类漂移对它无效。
- 证据:`packages/protocol/src/capabilities/manifest/system.ts` 中 AuditL2 条目。
- 期望行为:补齐四种 action 各自的返回结构(status 的策略/开关状态、enable 的结果、collect 的条目与归一化 schema、verify 的链校验结果),与 `docs/AUDIT-L2.md` §4 的归一化 schema 对齐;补全后既有守卫自动生效。
- 验收标准:① returns_schema 完整枚举顶层属性;② `console-contract.test.mjs` 对该能力反向守卫生效(或显式记录豁免理由);③ 旧控制端不受影响(返回结构只增不减——这里是补文档性契约,不改运行时返回)。
- 影响面:L0/L1 交界(manifest 变更,随被控端发版;运行时行为不变)。
**P1-5 L2 无 e2e 覆盖**
- 问题:`tests/e2e/run.mjs` 与 `windows.mjs` grep `l2` 均 0 命中——L2 只有单测(归一化/链校验/独立落盘),无「经真实 RPC 调用」的链路验证。
- 证据:上述 grep;`tests/unit/audit-l2.test.mjs` 覆盖的是模块级函数。
- 期望行为:e2e 增加 `system.audit.l2` 用例:status 在无策略环境返回可解析结构;collect 对空 Security 日志不崩、带 since/limit;verify 对自造 L2 链文件判定正确;非 Windows 平台(CI 的 Linux runner)明确报平台不支持。
- 验收标准:① 新增用例进 `run.mjs`(跨平台可跑部分)+ `windows.mjs`(Windows 专属部分);② 全绿;③ 与 33+13 基线不冲突(端口/临时 HOME 隔离遵守既有纪律)。
- 影响面:L2(测试)。
**P1-6 `na-gui` 陈旧计划任务清理(影响面窄、成本低)**
- 问题:历史测试机残留的 `na-gui` 计划任务会抢占 8765,导致控制台被控端起不来;当前只有单实例保护与「如何找出占用者」的报错提示,**没有清理路径**——用户拿到提示后仍要自己动手。
- 证据:`apps/console/src-tauri/src/agent.rs:1084`(测试注释引为动机);`scripts/uninstall.ps1` 无相关清理;CHANGELOG「控制台单实例保护」节自陈该任务「也在抢同一个端口」。
- 期望行为:`uninstall.ps1` / `control.ps1` 卸载与修复路径中,对 `na-gui`(及 nodeagent 自建的已知任务名)做存在性检查并清理,动作进日志。
- 验收标准:① 清理逻辑只针对白名单任务名,不做通配删除;② 有单测或脚本级验证(建同名任务→跑清理→断言已删);③ 不动其他计划任务。
- 影响面:L2 脚本(仅安装/卸载链路,不动协议)。
#### P2(优化项)
| # | 项 | 问题/证据 | 期望 | 影响面 |
|---|---|---|---|---|
| P2-1 | 控制台自身自更新通道 | `latest.json` 仅含 agent 资产(`scripts/release.mjs`),控制台 `check_update` 只能比对版本+更新内置 agent.mjs,无法下载/应用控制台安装包(sidecar `main.mjs:260`) | `latest.json` 增补 console 安装包资产(url+sha256),sidecar 增下载+校验+拉起安装器;**必须**沿用「sha256 不校验不落盘」铁律 | L1(大特性,需单独一轮) |
| P2-2 | 大文件拆分 | `apps/mcp/src/index.ts` 530 行、`tools/defs.ts` 618 行;`apps/agent/src/capabilities/window.ts` 963 行(ROADMAP 记载) | 按领域拆模块,纯移动不改行为;每次拆分跑全量测试 | L0/L1(PATCH 级) |
| P2-3 | 协议模糊测试 | DELIVERY-PIPELINE §3:畸形报文是枚举式,未做随机化 fuzz | 加一轮「随机字节+随机 JSON 结构」fuzz,断言「不崩、不留未审计入口」 | L2(测试) |
| P2-4 | sidecar CONNECT 隧道 TLS 叠层离线用例 | CHANGELOG 自陈:无受信任证书的本地 TLS 源站夹具,该路径无回归守卫 | 造证书夹具或采购测试证书,补离线用例 | L2(测试) |
| P2-5 | 存量覆盖率提升 | 全量 66.9%(DELIVERY-PIPELINE §8) | 只观测不设全量硬指标;靠变更门禁(≥70%)自然抬升;GUI 注入等无头不可验模块显式豁免并记录理由 | L2 |
| P2-6 | 分支保护 | `main` protection 返回 404(DELIVERY-PIPELINE §8 已知 P1,非代码项) | 仓库设置开启「PR + CI 全绿 + 禁止直推」;需维护者在 GitHub 侧操作 | 流程 |
| P2-7 | Linux/macOS L2 与 Sysmon 并用 | AUDIT-L2.md §3.2/§3.3:Linux 需预装 auditd;macOS ESF 受 entitlement 限制;Sysmon 为第三方驱动 | 后续版本按「部署侧前置条件」成熟度逐端评估;不写「假装能用」的代码 | L0(后续) |
### 3.2 UI 设计草案(仅本轮新增部分)
- **审计页 L2 区**(控制台,`apps/console`):在既有「审计」页内增加分区,不新增顶级路由。
- 上部:L2 状态卡(策略是否启用、`SCENoApplyLegacyAuditPolicy`、`ProcessCreationIncludeCmdLine_Enabled` 两个关键开关的✓/✗与一句提示);
- 下部:操作行(collect:since/limit 输入 + 「采集」按钮;verify:「校验 L2 链」按钮)与结果表(归一化条目:ts/kind/subject/object/event_id);
- 非 Windows 被控端:整区置灰 + 文案说明(复用既有平台门控模式,与软件页/服务页一致);
- 界面纪律:纯文字导航不用图标;配色只吃 `--na-*` 变量;`emit`/`listen` 事件名双向一致(契约守卫自动覆盖)。
- **CLI**:`nodeagent rollback [--dry-run]` 输出三段式:LKG 记录(hash/bytes/recorded_at)→ 与当前指纹比对 → 执行结果与复核提示;复用既有 `deploy` 的输出风格。
- **MCP**:`na_audit_l2` 工具描述写清信任边界(本地副本、防篡改靠转发出机),避免 AI 误用为「不可篡改证据」。
### 3.3 待确认问题(需需求方确认,PRD 不擅自假设)
| # | 问题 | 背景与建议 |
|---|---|---|
| Q1 | **版本策略**:本轮含被控端变更(P0-1 selfupdate)+ 新能力面(P1-1/P1-4 L2 契约与 MCP 工具),按 `docs/VERSIONING.md` 新能力 = MINOR。建议一次性 **v2.1.0** 发版(而非 2.0.4),L2 从「已在 agent 未发版」转为正式发布能力。是否批准? | 若拆成 2.0.4(selfupdate)+ 2.1.0(L2 周边)两发,则中间态 agent 与文档会再次短期脱节 |
| Q2 | **selfupdate 代理范围**:A) 全量复用 sidecar 方案(环境变量 + 系统代理发现 + 明确错误,被控端新增出网模块);B) 最小集(仅 `NA_PROXY`/`HTTPS_PROXY` 环境变量 + 现有超时放大);C) 不改代码,仅文档引导用镜像 URL。建议 A——B 留「用户明明配了系统代理却白搭」的坑,C 把问题推给用户 | A 的代价:被控端新增约 150~250 行 + 单测;需评估 agent 包体积预算 |
| Q3 | **「契约漂移 6 项(缺 seq/session_id/risk 等字段)」的出处**:仓库内未找到该台账(protocol 源 grep 三字段 0 命中;console-contract 守卫 20 用例在役)。请提供台账来源;若指「审计条目应有 seq/session_id/risk 字段」,则给 `AuditEntry` 加字段属返回结构只增(可行),但需一并定义消费方(控制台审计页/MCP na_audit)与脱敏口径——是否纳入本轮 P1? | 在澄清前,本 PRD 只将「L2 returns_schema 空壳」(P1-4)与「文档数字漂移」(P1-3)列为实证漂移 |
| Q4 | **L2 是否本期接 MCP/控制台**(P1-1/P1-2):agent 已落地、CLI 已有入口;MCP 与控制台补齐属「只增」。建议本期做——否则 AI 接入方与图形用户完全看不到 52 项中的第 52 项 | 若砍,则 G2「三端完整」目标降级为「agent+CLI 完整」,需需求方确认接受 |
| Q5 | **na-gui 清理是否本期做**(P1-6):影响面仅历史测试机,但属「用户拿到报错后仍无法自救」的最后一公里。建议做(成本约半天) | — |
| Q6 | **P2 项是否全部出到后续版本**:P2-1(控制台自更新)是较大的独立特性,建议明确排期到 v2.2.0,不在本轮夹带 | 避免本轮范围膨胀 |
## 4. 附:本轮交付物流转(供阶段 2/3 参考)
1. 阶段 2(架构师):按需求池逐条出「修改方案 + 代码示例」,P0 先行;契约类改动(P1-4)先落 manifest 再动实现。
2. 阶段 3(开发):P0 → P1 → P2;每个 P0/P1 项完工即补「会红的守卫」(本项目铁律:没有变成断言的修复等于没有修复)。
3. 阶段 4(测试):回归 357 单测 + e2e 33+13 + Rust 18;新增 L2 e2e;发版前按 DELIVERY-PIPELINE §5 真机矩阵(Windows x64 必跑、arm64 人工必查)与 §7 发布后复核清单执行。
4. 发版:按 Q1 结论执行 `pnpm release`;被控端变更一律升号,控制台变更重新构建分发安装包(console-release.yml 已随 tag 自动挂 4 包)。
+68
View File
@@ -0,0 +1,68 @@
classDiagram
class ProxyModule {
<<module apps/agent/src/net/proxy.ts>>
+normalizeProxyUrl(raw) URL|null
+isBypassed(targetUrl, noProxy) boolean
+pickEnvProxy(env, targetUrl) EnvPick
+parseScutilProxy(text) SysProxy|null
+parseRegQueryProxy(text) SysProxy|null
+detectSystemProxy(platform, run) Promise~SysProxy|null~
+resolveProxy(opts) Promise~ProxyDecision~
+httpGet(url, opts) Promise~HttpResponse~
+tunnelConnection(proxy, target, signal) Function
}
class SelfUpdateCapability {
<<module apps/agent/src/capabilities/selfupdate.ts>>
+agentUpdate(args) Promise~unknown~
-download(url, timeoutMs) Promise~DownloadResult~
-planBackupCleanup(files, keep) string[]
}
class LkgRecord {
<<interface apps/agent/src/lkg.ts>>
+hash string
+hash_full string?
+bytes number
+script_path string
+backup_glob string?
+recorded_at number
+node_version string
+platform string
}
class LkgModule {
<<module apps/agent/src/lkg.ts>>
+recordLKG() LkgRecord|null
+readLKG() LkgRecord|null
+lkgStatus() LkgStatus
+lkgFilePath() string
}
class RollbackCommand {
<<module apps/cli/src/cmd/rollback.ts>>
+cmdRollback(opts) Promise~void~
+planRollback(info) RollbackPlan
+pickBackupByHash(candidates, wantHash) Candidate|null
+hashRemoteFile(client, path) Promise~FileHash~
}
class AuditL2Capability {
<<module apps/agent/src/capabilities/audit-l2.ts>>
+auditL2(args) Promise~unknown~
+verifyL2() L2VerifyResult
+readL2(limit) L2Entry[]
+l2FilePath() string
}
class ManifestSystem {
<<module packages/protocol/src/capabilities/manifest/system.ts>>
+SYSTEM_CAPABILITIES CapabilityDescriptor[]
}
SelfUpdateCapability --> ProxyModule : 单向依赖
SelfUpdateCapability --> LkgModule : 读 LKG(回滚提示)
LkgModule --> LkgRecord : 读写
RollbackCommand --> LkgModule : 经 system.info.lkg 消费
RollbackCommand ..> AuditL2Capability : 无直接依赖(仅同库)
ManifestSystem ..> AuditL2Capability : returns_schema 契约真源
+21
View File
@@ -0,0 +1,21 @@
sequenceDiagram
participant CLI as 控制端 (CLI/MCP/console)
participant S as agent server.ts
participant SU as selfupdate.ts
participant PX as net/proxy.ts
participant NET as node:http/https
CLI->>S: invoke system.agent.update {url, sha256, timeout_ms}
S->>SU: agentUpdate(args)
SU->>PX: resolveProxy({targetUrl: url})
PX->>PX: NA_PROXY → env → 系统代理(reg/scutil)
PX-->>SU: {proxy, source, notes}
SU->>PX: httpGet(url, {proxy, timeoutMs})
PX->>NET: CONNECT 隧道 / 绝对形态 / 直连
NET-->>PX: 响应字节流(链共享 AbortController)
PX-->>SU: {status, body}
SU->>SU: sha256 校验(不符→丢弃,不落盘)
SU->>SU: 备份 → 就地覆写 → 写后校验(不符→回滚)
SU->>S: agentRestart({delay_ms:1500})
SU-->>S: {updated, via, proxy, proxy_source, incoming_hash, ...}
S-->>CLI: {status:'ok', data}