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"的老写法