From ab7a2b147b9e67b379bfa0456a6d27e78a5dedf1 Mon Sep 17 00:00:00 2001 From: jkooe <62494645+jkooe@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:50:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(v26):=20=E8=87=AA=E9=87=8D=E5=90=AF?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=B8=85=E7=90=86=E9=98=88=E5=80=BC=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E3=80=8C=E6=9C=AC=20agent=20=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=97=B6=E5=88=BB=E3=80=8D=EF=BC=88=E7=9C=9F=E6=9C=BA=E5=AE=9E?= =?UTF-8?q?=E6=B5=8B=201=20=E5=B0=8F=E6=97=B6=E7=B2=97=E9=98=88=E5=80=BC?= =?UTF-8?q?=E4=BB=8D=E4=BC=9A=E7=B4=AF=E7=A7=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 真机发现(Parallels Win11 ARM) restart 两次后仍有 **2 个** selfrestart 任务(旧的 Ready 未被清)。诊断确认: - 清理**条件**能选中它 ✓(非匹配问题) - **手工删除**成功 ✓(非权限问题) → 算时间戳才明白:那是「1 小时」粗阈值的**必然结果** —— 旧任务创建于 00:44, 第二次 restart 在 00:56(仅隔 12 分钟),清理那一刻它**仍是 Running**(cmd 还持有上一次的 agent), 不满足「非 Running 或早于 1 小时」→ 保留。 **但规则本身是缺陷**:频繁 restart(间隔 < 1h)时旧任务照样累积 —— v26 想治的僵尸只是从「永不清理」变成「1 小时后才清」。 ## 改法(语义更精确) ```powershell # 旧:$cut = (Get-Date).AddHours(-1) ← 粗略 # 新:$cut = (Get-Date).AddMilliseconds(-) ← 「本 agent 的启动时刻」 ``` 凡在**我启动之前**就运行的任务,必然不是我的宿主(我的宿主刚刚才创建)→ 可安全删除。 ## 验证(真机闭环) 1. `nodeagent deploy release/agent.mjs` → 指纹校验 + 备份 + 上传 + 重启 + 校验 ✓ (顺带把 deploy 机制也验了) 2. `restart` → **任务只剩 1 个**(Running,当前宿主)→ **旧任务被精确清理** ✓✓ 3. 单测:断言必须是「agent 启动时刻」阈值,且**禁止回退到 AddHours(-1)**; 守卫 9/9 + 全量 274 绿;7 包构建通过 --- apps/agent/src/capabilities/system.ts | 13 ++++++++++--- docs/VM-VERIFICATION.md | 2 +- tests/unit/security-hardening.test.mjs | 6 ++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/agent/src/capabilities/system.ts b/apps/agent/src/capabilities/system.ts index 112171e..7242261 100644 --- a/apps/agent/src/capabilities/system.ts +++ b/apps/agent/src/capabilities/system.ts @@ -534,9 +534,16 @@ async function scheduleWindowsRestart(p: RestartParams): Promise { // (= 自杀,表现为"restart 成功但 agent 消失")。原实现只清非 Running, // 代价是真机累积了 3 个 Running 僵尸(2026-10-09 实测)。 // - // 现策略:非 Running **或** LastRunTime 早于 1 小时 → 清。 - // 1 小时余量远大于一次重启的耗时,因此"当前宿主"绝不会被误判(它刚刚才运行)。 - `$cut = (Get-Date).AddHours(-1)`, + // 现策略:非 Running **或** LastRunTime **早于"本 agent 启动时刻"** → 清。 + // + // ⚠️ 2026-10-11 真机(Parallels Win11 ARM)修正:原先用「早于 1 小时」作阈值, + // 实测发现**频繁 restart(间隔 < 1h)时旧任务照样累积**(v26 想治的僵尸只是从 + // "永不清理"变成"1 小时后才清")。根因是阈值选得太粗: + // 阈值应表达「**这个任务是不是我(当前 agent)的宿主**」—— + // 凡在**我启动之前**就运行的任务,必然不是我的宿主(我的宿主是刚刚才创建的), + // 因此可以安全删除。用 uptime 换算出的启动时刻正是这个边界。 + `$agentStart = (Get-Date).AddMilliseconds(-${Math.max(0, Math.round(process.uptime() * 1000))})`, + `$cut = $agentStart`, `Get-ScheduledTask -TaskName 'nodeagent-selfrestart*' -ErrorAction SilentlyContinue | Where-Object { if ($_.State -ne 'Running') { return $true }; $ti = ($_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue); if ($ti -and $ti.LastRunTime -and $ti.LastRunTime -lt $cut) { return $true }; return $false } | Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue`, `$a = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument '${cmdLine.replace(/'/g, "''")}'`, // 不限时(默认 72h 后强制结束),保证 node 能长期运行 diff --git a/docs/VM-VERIFICATION.md b/docs/VM-VERIFICATION.md index 9f6c7a7..1450e8a 100644 --- a/docs/VM-VERIFICATION.md +++ b/docs/VM-VERIFICATION.md @@ -215,7 +215,7 @@ ARM Windows(含 Parallels Win11 ARM)请用 **arm64 包**;用 x64 包会卡 | B1 v1.5 四项 | ✅ 全过(含 `route=post` 实测命中 138 窗口) | | B4 锚点端到端 | ✅ 全过(含上传云盘) | | B2 v23 三防护 | ⏸ 阻塞:需改配置 + restart,而计划任务当前启动失败 | -| B3 v26 清理 | ⏸ 同上 | +| B3 v26 清理 | ✅ **验证并改进了策略**:实测发现「1 小时」粗阈值下频繁 restart 仍累积 → 改为「**早于本 agent 启动时刻**」精确阈值(部署到 VM 实测:restart 后旧任务被自动清理、宿主未误杀) | ### 实测新增的排查项(补进 §6) diff --git a/tests/unit/security-hardening.test.mjs b/tests/unit/security-hardening.test.mjs index cb6128b..5ebab2b 100644 --- a/tests/unit/security-hardening.test.mjs +++ b/tests/unit/security-hardening.test.mjs @@ -85,11 +85,13 @@ const restartSrc = readFileSync( 'utf8', ); -test('自重启任务清理:必须带「运行超 1 小时」余量,禁止简单清所有 Running', () => { +test('自重启任务清理:阈值必须是「本 agent 启动时刻」(精确),禁止 1 小时粗阈值', () => { // 背景:重启任务的 cmd 长期持有新 agent 进程 → 任务状态长期 Running; // 若把 Running 的全清掉会连当前 agent 的宿主一起杀(= 自杀)。 // 真机 2026-10-09 实测残留 3 个 Running 僵尸,故引入时间余量判定。 - assert.ok(restartSrc.includes('AddHours(-1)'), '缺少 1 小时余量判定'); + assert.ok(restartSrc.includes('$agentStart = (Get-Date).AddMilliseconds(-'), '缺少「本 agent 启动时刻」阈值'); + assert.ok(restartSrc.includes('$cut = $agentStart'), '阈值应取自 agent 启动时刻'); + assert.ok(!restartSrc.includes('AddHours(-1)'), '不应再回退到「1 小时」粗阈值(真机实测会累积)'); assert.ok(restartSrc.includes('LastRunTime -lt $cut'), '必须按 LastRunTime 与阈值比较'); assert.ok(restartSrc.includes('Get-ScheduledTaskInfo'), '需要读取任务的运行信息'); // 禁止回到"只清非 Running"的老写法