diff --git a/README.md b/README.md index f9b13af..69abde2 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ nodeagent 把一台 Windows 机器的能力**标准化成一组可授权、可 | **输入** | `input.mouse.move` · `input.mouse.click` · `input.mouse.scroll` · `input.mouse.drag` · `input.key.type` · `input.key.press` | 键鼠控制(🔒 **默认禁用**),含**拖拽**、**字符串热键 / 预设 / 长按 / 后台定向投递** | | **剪贴板** | `clip.get` · `clip.set` | 读写文本**或图片**(PNG Base64) | | **事件订阅** | `event.watch` · `event.unwatch` · `event.list` · `event.poll` | 文件变动 / 进程启停 / 端口开闭,**主动推送**(无需轮询) | +| **GUI 等待** | `gui.await` | 等条件成立再返回(窗口/元素/进程/文件 × 出现/消失);支持**属性谓词 `where`** 与 **`any_of` 组合** | +| **状态采样** | `log.query` · `monitor.start` · `monitor.report` · `monitor.stop` · `monitor.list` · `monitor.delete` | 日志**在被控端侧过滤**(不上传整份)/ 定时采样落盘 + **摘要回看**(断几次、最长断多久、CPU/内存值域) | +| **审计锚定** | `system.audit.head` · `system.audit.anchor` | 链头可读 + 锚点**追加**落盘,让「有 root 者整链重写」可被发现 | ## 快捷键(hotkey)控制 —— v1.5 @@ -102,7 +105,70 @@ nodeagent --node win invoke gui.await --args '{"condition":"file","path":"C:\\lo **典型闭环**:`await control(text:"立即安装")` → `mouse.click(x, y)` → `await control(text:"完成", timeout_ms:60000)` → `click` → `await absent(text:"安装中")`。 -**43 项能力** · **38 个 MCP 工具** · **215 项单元测试** · **30 项端到端用例** · **11 项 Rust 单测**(CI 在真实 Windows / Linux runner 上验证) +### 属性谓词与组合(v1.7) + +`screen.find` 现在返回 UIA 属性,`gui.await` 可据此等**状态**而不只是「出现」: + +```bash +# 等一个「可用」的按钮(不指定文字) +nodeagent --node win await --condition control --where '{"enabled":true}' +# 等值里含「已」的元素 +nodeagent --node win await --condition control --where '{"value":"*已*"}' --timeout 15000 +# 组合:装成功 或 报错,先出现的算 +nodeagent --node win await --any_of '[{"condition":"window","title":"安装完成"},{"condition":"control","text":"错误"}]' +``` + +| 属性 | 来源 | 拿不到时 | +|---|---|---| +| `enabled` | `IsEnabled` | — | +| `value` | `ValuePattern` | `null`(**不猜**) | +| `selected` | `SelectionItemPattern` | `null` | +| `toggle` | `TogglePattern` | `null` | + +> 属性只在 **UIA 引擎**下可读(Windows)。走 OCR/图像模板时 `where` 被忽略并**带回 `note`**, +> 不会让调用方误以为「按属性过滤过了」。macOS 无 UIA 等价物,`where` 不可用(会明确报错提示用 `text`)。 + +--- + +## 状态采样:日志过滤与定时采样 —— v1.8 + +治**间歇性**问题(代理不通、杀软拦截、端口时开时闭)—— `system.status` 只能看当下快照, +而 `event.watch` 只解决「有事件时通知」,没有「**持续记录并回看**」。 + +```bash +# 日志在被控端侧过滤,只回匹配行(几万行日志也不整份拉回) +nodeagent --node win log --path "C:\app\app.log" --level ERROR --limit 20 +nodeagent --node win log --path "C:\app\app.log" --pattern "timeout|refused" --tail + +# 连续采样 5 分钟,事后回看「断过几次、最长断多久」 +nodeagent --node win invoke monitor.start --args '{"source":"port","target":"127.0.0.1:8765","interval_ms":2000,"id":"probe"}' +nodeagent --node win monitor report probe +nodeagent --node win monitor stop probe +``` + +采样源四类:`port`(连通性 + 延迟)/ `process`(存活 + PID)/ `command`(退出码 + 首行输出)/ +`metric`(CPU·内存)。样本落 `<数据目录>/monitors/.jsonl`,**摘要直接给结论**。 + +--- + +## 审计链外部锚定 —— v25 + +链内哈希只能发现「改一条」。**有 root 的攻击者可以整链重写** —— 从零重建一份自洽的假日志, +`audit.verify` 照样通过。外部锚定是唯一能戳破它的手段:把链头记到**链外**。 + +```bash +nodeagent --node win audit head --compare # 看链头 + 与最近锚点比对 +nodeagent --node win audit anchor --note "每日例行" # 追加一个锚点 +``` + +**锚点记四元组** `{entries, head_hash, head_ts, rotated_segments}`,不能只记哈希 —— +日志轮转会丢弃最旧段使条目数下降,只比哈希会把**轮转误判成篡改**。 +锚点文件**只追加**(可覆盖的话,攻击者重写链后再刷新锚点即可抹痕)。 + +> 真正发挥价值的前提:把锚点文件指向**另一台机器 / 网盘同步目录**。 +> 本机文件被整盘重写时,锚点也随之消失 —— 那就等于没锚。 + +**51 项能力** · **41 个 MCP 工具** · **269 项单元测试** · **30 项端到端用例** · **9 项 Rust 单测**(CI 在真实 Windows / Linux runner 上验证) > 关键里程碑:**GUI 语义**(`window.list` + `screen.find`,UIA 找不到自动降级 OCR) > 让 AI 从「看得到画面但读不懂界面」变成「按名字取坐标点下去」。 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0ceb5b5..2c2027f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,8 +1,9 @@ # nodeagent 进度盘点与下一步路线 -> 盘点日期:2026-10-07(**第三次更新**:v1.6.0 已发、CI 全绿后据实刷新) -> 盘点方式:只读核查仓库(`git fetch` + 源码 grep + 本地跑单测 + `gh run list`) -> 基线:远端 main(tag **v1.6.0** 之后)|单测 **221 全绿** · e2e **30 全绿** · Windows CI **13/13 全绿** +> 盘点日期:2026-10-10(**第四次更新**:语义/状态/安全三个方向全部落地后据实刷新) +> 盘点方式:真机验证 + 本地跑单测 + `gh run list`(Windows 被控端曾于 10-10 00:15 离线) +> 基线:远端 main(`9ad296b` 之后)|单测 **269 全绿** · e2e **30 全绿** · 能力 **51 项** · Windows CI 全绿 +> ⚠️ 全部**未发版**(皇上要求先攒着)—— 下次发版建议直接跳 **v2.0.0** > 适用范围:本文档随仓库公开,**不含任何真机地址与账户信息**(见 §6 脱敏约定) --- @@ -374,6 +375,35 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> ## 九、进展台账(2026-10-07 第三/四次盘点后完成) +### 10-09 ~ 10-10 追加(语义 / 状态 / 安全三方向收尾) + +| 事项 | 状态 | 说明 | +|---|---|---| +| 语义方向:`screen.find` 读 UIA 属性 + `where` 谓词 + `gui.await` 组合 | ✅ | `enabled/value/selected/toggle`;拿不到留 `null` 不猜;`any_of` 组合;能力数 43→49 | +| 状态方向:`log.query` + `monitor.*`(5 项) | ✅ | 日志被控端侧过滤;采样落盘 + 摘要回看(断几次/最长断多久/CPU·内存值域) | +| 安全:连接层三防护(握手封禁 / 连接上限 / 空闲断开) | ✅ | 本地真机三场景验证(正确密钥可连 → 连续错误被拒 → 封禁期正确密钥也被拒) | +| 安全:安装期密钥改 stdin | ✅ | `install.cmd` 管道喂 `-KeyFromStdin`,不再出现在命令行 | +| 安全:`ed25519` 默认化 → **零信任优先安装** | ✅ | 有 `client-acl.json` 即走 ed25519(`default_effect=deny`);否则 psk + 醒目告警 | +| 安全:**审计链外部锚定** | ✅ | `system.audit.head` + `system.audit.anchor`;四元组锚点防轮转误判、追加式防刷新抹痕;能力数 49→51 | +| 自重启在**非管理员**身份下的降级 | ✅ | 真机坐实:`-RunLevel Highest` 被拒 → 回退不指定 Principal;`deploy` 由此首次一次走通 | +| deploy 备份不再堆积 | ✅ | `.bak-` → 固定名 `.bak` + 清理历史;真机 17 个文件 → 2 个 | +| 自重启任务 Running 僵尸清理 | ✅ 代码 / ⏳ 真机 | 清理条件加「LastRunTime 早于 1 小时」余量(防误杀当前宿主) | +| **`SECURITY.md` §5「还没做的」清单** | ✅ **全部完成** | 见 §4.5~§4.8 | + +### 真机验证过程中抓到的真 bug(8 个,全部只有真机能发现) + +| # | bug | 性质 | +|---|---|---| +| 1 | `install.cmd` 双击首装即炸(`for /f` 调 PS,`^|` 漏进 PowerShell) | **用户首装必炸** | +| 2 | `pack.mjs` 三连(spawn `.bin/esbuild` ENOENT / `zip` / `ls -la`+`du -sh`) | 打包脚本全平台假设 | +| 3 | `where` 的 PS 注入语法错(连修**两轮**才通:JSON 字面量 ✗ → `\"` 转义 ✗ → PS 单引号 ✓) | 跨语言转义 | +| 4 | `screen.find` 的 where-only 被自己入口校验拦下 | v1.7 只改了一半 | +| 5 | 自重启在非管理员下失败 → `deploy` 永远"半成功" | v7 起的既有缺陷 | +| 6 | `parseLeadingTs` 吃掉 ISO 的 `Z` 时区标记(偏 8 小时) | 单测抓到 | +| 7 | manifest 承诺的 `note` 字段代码没实现 | 承诺未落地 | +| 8 | 锚定动作自身写审计导致"紧随比对恒显示已增长" | 语义瑕疵(已说明) | + + | 事项 | 状态 | 说明 | |---|---|---| | `gui.await` 收尾(断言/单测/入口/版本号/本地真机/发版) | ✅ | v1.6.0 |