dsh-remote-agent 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +370 -0
- package/README.en.md +95 -4
- package/README.md +147 -40
- package/cordis.patch.yml +48 -1
- package/docs/workspace-share.md +141 -0
- package/lib/client.js +392 -13
- package/lib/env-check.js +244 -0
- package/lib/hub/index.js +89 -1
- package/lib/hub/node-registry.js +96 -2
- package/lib/hub/registry-ingest.js +15 -0
- package/lib/hub/tools.js +26 -1
- package/lib/hub/ui-routes.js +157 -1
- package/lib/index.js +56 -7
- package/lib/node/fs-service.js +20 -2
- package/lib/node/index.js +86 -1
- package/lib/node/runner.js +23 -1
- package/lib/node/share-service.js +213 -0
- package/lib/node/transport.js +54 -1
- package/lib/protocol.js +18 -0
- package/lib/relay.js +18 -1
- package/lib/setup-routes.js +51 -5
- package/package.json +1 -1
- package/scripts/check-doc-drift.mjs +169 -3
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,376 @@
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [0.9.0] - 2026-10-17
|
|
13
|
+
|
|
14
|
+
### ⚠ 行为变更:exec 任务默认超时 1 小时 → 10 分钟
|
|
15
|
+
|
|
16
|
+
**升级前请读这一段。**
|
|
17
|
+
|
|
18
|
+
此前 `exec` 任务实际拿到的超时是 **60 分钟**(不论文档说什么)。
|
|
19
|
+
原因是三处缺陷叠加(详见下方「代码审查修复」的 H1):
|
|
20
|
+
|
|
21
|
+
1. `tools.js` 里有个 TDZ 自引用 bug,让分模式默认值那条分支永远走不到
|
|
22
|
+
2. `hub/index.js` 用 `numOr(defaults.timeoutMs, 1h)` 兜底,把 `explicitTimeoutMs`
|
|
23
|
+
压成恒 3600000 → 优先级 `args > 显式配置 > 分模式默认` 被压扁成「恒 1h」
|
|
24
|
+
3. 面板 dispatch 同样写死 `?? 3600000`
|
|
25
|
+
|
|
26
|
+
三处都修了,所以现在:
|
|
27
|
+
|
|
28
|
+
| 模式 | 默认超时 | 意味着 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `exec` | **10 分钟**(`EXEC_DEFAULT_TIMEOUT_MS`) | 失控命令最多跑 10 分钟就被杀,不再占机一小时 |
|
|
31
|
+
| `agent` | 1 小时(不变) | agent 任务本来就需要长窗口 |
|
|
32
|
+
|
|
33
|
+
**如果你有 exec 任务本来就要跑 10 分钟以上**,显式传 `timeoutMs`:
|
|
34
|
+
|
|
35
|
+
- 工具:`remote_task({ ..., timeoutMs: 1800000 })`(30 分钟)
|
|
36
|
+
- 面板:派发表单的「超时毫秒」字段
|
|
37
|
+
- 配置:`defaults.timeoutMs`(作用于所有任务,优先于分模式默认)
|
|
38
|
+
|
|
39
|
+
### 修复:代码审查(1 High + 3 Medium + 6 Low)
|
|
40
|
+
|
|
41
|
+
WorkBuddy 2026-10-17 对 `lib/` 全部 26 个源文件(约 19,900 行)做全量审查,
|
|
42
|
+
发现的缺陷逐条核实为真后修复。
|
|
43
|
+
|
|
44
|
+
#### H1(High)tools.js TDZ 自引用
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const v = validateTaskStart({ constraints: {
|
|
48
|
+
timeoutMs: ... ?? (v.spec.mode === 'exec' ? ... : ...)
|
|
49
|
+
^ v 正在初始化,进入 TDZ
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`??` 链左侧全空时抛 `ReferenceError`。
|
|
53
|
+
|
|
54
|
+
**为什么 829 个测试全绿仍带此 bug**:e2e helper 恒传 `timeoutMs`(`??` 第一项
|
|
55
|
+
短路)、单测构造时 defaults 也总有值 —— 该分支**从未被求值**。这是典型的
|
|
56
|
+
「测试环境与生产装配层差异造成的盲区」。
|
|
57
|
+
|
|
58
|
+
生产没炸但更糟:`normalizeConfig` 的 1h 兜底让 TDZ 永不触发,却把分模式
|
|
59
|
+
默认压成死代码(见上方行为变更)。
|
|
60
|
+
|
|
61
|
+
修复:改用已在 `v` 之前求值的 `routeMode`。
|
|
62
|
+
|
|
63
|
+
#### M1(Medium)relay 第三条 hub 写路径漏背压
|
|
64
|
+
|
|
65
|
+
C1-1 的背压修复只加在 `broadcastHubs` / `broadcastHubsBinary` 两条路径上,
|
|
66
|
+
**node 上行帧 → 全 hub 广播是第三条**,漏网。而 `ws.js` 的 `overBudget` 只是
|
|
67
|
+
查询属性、不自动断连,所以这条路径的写缓冲**没有任何上限检查**。
|
|
68
|
+
|
|
69
|
+
实测过的后果形态:单个停滞对端把 relay `writableLength` 推到 339.8 MiB、
|
|
70
|
+
RSS 419 MiB。中继是 NAT 场景唯一会合点、单进程常驻,OOM 即全体断连。
|
|
71
|
+
|
|
72
|
+
#### M2(Medium)grep 单文件分支无大小护栏
|
|
73
|
+
|
|
74
|
+
目录扫描路径有 `GREP_FILE_MAX`(2MB)与 `GREP_TIME_MS`(10s)双护栏,
|
|
75
|
+
但 `st.isFile()` 分支**直接 `readFileSync` 全文**。触发形态:`remote_grep` 的
|
|
76
|
+
path 指向工作区内巨大文本文件(应用日志 / 数据库转储 / core dump)→
|
|
77
|
+
副机 OOM + 同步阻塞事件循环。**这是一条可达的 DoS 面**(agent 自主选择 grep 目标)。
|
|
78
|
+
|
|
79
|
+
#### M3(Medium)approveTask 节点不可达时删除任务
|
|
80
|
+
|
|
81
|
+
原实现 `this.tasks.delete(taskId)` —— 任务从 hub 视图**彻底消失**(不是失败、
|
|
82
|
+
不是挂起,是不存在),与该方法注释自称的「转 failed 终态」**直接矛盾**。
|
|
83
|
+
|
|
84
|
+
后果:`remote_status` / 面板都查不到;`awaitTerminal` 等不到终态只能靠超时
|
|
85
|
+
兜底(最长 1 小时+);与 ui-routes 的 502 文案不符。
|
|
86
|
+
|
|
87
|
+
修复:保留记录 + 置 FAILED + `error.code='node-unreachable'` + 唤醒等待者。
|
|
88
|
+
|
|
89
|
+
#### Low 系列(修 6 条)
|
|
90
|
+
|
|
91
|
+
- **L1** 副机 transport 缺 RelayLink 的两条并发不变式:
|
|
92
|
+
`connecting` 闸门(`start()` 被调两次会开两条 TCP 连接)、close 必须校验
|
|
93
|
+
`this.conn === conn`(否则陈旧连接的 close 把活着的新连接引用清掉 → 副机永久失联)
|
|
94
|
+
- **L2** RESOURCE 分支 `rec` 未判空(影子节点竞态必抛 TypeError)
|
|
95
|
+
- **L3** runner 的 active 注册晚于 `await createAgent`(窗口期内 cancel 返回
|
|
96
|
+
false,正是 #26-5 修过的形态复现)→ 改为先占位再 await
|
|
97
|
+
- **L4** `E2E_REOFFER_DEBOUNCE_MS` 死常量 → 真正按 250ms 时间窗去抖
|
|
98
|
+
(⚠ 它是**实例字段**不是 static,必须经 `this.` 读)
|
|
99
|
+
- **L5/L6/L7 未修**(记录在案):relay 模式 hub 去重被中继层兜住、WS 版本校验
|
|
100
|
+
双方恒发 13、hub 空闲淘汰被副机 15s 心跳掩盖 —— 属一致性改进而非缺陷
|
|
101
|
+
|
|
102
|
+
#### 测试
|
|
103
|
+
|
|
104
|
+
新增 `test/code-review-regressions.test.js` **11 条**,覆盖 H1/M1/M2/M3/L1/L2/L4。
|
|
105
|
+
**H1 的测试刻意不走 e2e helper**(那正是盲区成因)。
|
|
106
|
+
|
|
107
|
+
### 文档:README 补「功能」章节
|
|
108
|
+
|
|
109
|
+
用户指出「README 好像没有功能相关的介绍」—— 核实后发现**比这更严重**:
|
|
110
|
+
|
|
111
|
+
- **版本号落后 4 个版本**(写 0.4.0,实际 0.8.x)
|
|
112
|
+
- **工具数量写错**(写「7 个」,实际 **12 个**:8 个 `remote_*` + 4 个 fs 工具),
|
|
113
|
+
出现在架构图、目录表、工具速查三处
|
|
114
|
+
- 确实没有功能总览章节
|
|
115
|
+
|
|
116
|
+
新增「功能」六组:派任务 / 拿结果 / 翻文件 / 本地 IDE 打开(SMB)/ 自我诊断 /
|
|
117
|
+
断连跨网安全。英文版同步。
|
|
118
|
+
|
|
119
|
+
新增断言 83:**README 声明的工具数量必须等于代码里 `reg(` + `fsOp(` 的注册数**,
|
|
120
|
+
两边任一变化立刻失败(做过负向验证)。
|
|
121
|
+
|
|
122
|
+
### 关于 0.8.0 与 0.8.1(记录)
|
|
123
|
+
|
|
124
|
+
0.8.0 的 publish 第一次报「假成功」(tarball 未落地),当时判断它卡在 staging
|
|
125
|
+
并跳过发布了 0.8.1。事后查 registry 发现 **0.8.0 最终也发布成功了**
|
|
126
|
+
(07:43:45,比 0.8.1 晚 3 分钟)—— 那次 `unpublish` 同样假成功。
|
|
127
|
+
|
|
128
|
+
两个版本内容基本相同(都是 #26-4/#26-5 + EPERM 修复 + #27 完整版),
|
|
129
|
+
不影响使用。教训:**npm publish 的成功/失败判定必须用 `Cache-Control: no-cache`
|
|
130
|
+
读 registry 核实**,不能只看 exit code 或缓存副本。
|
|
131
|
+
|
|
132
|
+
### 文档:README 补「功能」章节 + 修正 4 个版本号的漂移
|
|
133
|
+
|
|
134
|
+
用户指出「README 好像没有功能相关的介绍」—— 核实后发现**比这更严重**:
|
|
135
|
+
|
|
136
|
+
#### 1. 版本号落后 4 个版本
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
README L33: | **状态** | 0.4.0 —— 主机侧端到端可用 ... |
|
|
140
|
+
实际: npm latest = 0.8.1
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### 2. 工具数量写错(7 → 12)
|
|
144
|
+
|
|
145
|
+
README 写「7 个 remote_* 工具」,实际是 **12 个**(8 个 `remote_*` +
|
|
146
|
+
4 个 fs 工具)。这个数字在 3 处出现(架构图、目录表、工具速查)。
|
|
147
|
+
|
|
148
|
+
#### 3. 确实没有「功能」章节
|
|
149
|
+
|
|
150
|
+
原结构是「五分钟上手 → 核心不变式 → 安装 → 用法」——「用法」里只有工具速查,
|
|
151
|
+
**没有一个"这个插件能干什么"的总览**。读者只能从零散段落里拼。
|
|
152
|
+
|
|
153
|
+
#### 修复
|
|
154
|
+
|
|
155
|
+
新增「功能」章节,分六组:
|
|
156
|
+
|
|
157
|
+
1. **把任务派给副机** —— 两种模式 / 两阶段审批 / dry-run / 仓库感知 /
|
|
158
|
+
负载感知 / 模型下拉 / 分模式超时 / 并发上限
|
|
159
|
+
2. **拿结果但不搬整个工作区** —— handoff 卡 / 三分类 / 显式拉取 /
|
|
160
|
+
断点续传 / 变更 diff
|
|
161
|
+
3. **直接翻副机的文件** —— 四个只读工具(不进任务队列)
|
|
162
|
+
4. **用本地 IDE 打开副机文件** —— SMB 共享(含安全警示)
|
|
163
|
+
5. **出问题能自己诊断** —— 环境体检 / 可达性自检 / 队列健康 /
|
|
164
|
+
运行中分组 / 取消真打断 / 审计日志
|
|
165
|
+
6. **断连、跨网、安全** —— 断连不终止 / 两种传输 / E2E / 主机身份 /
|
|
166
|
+
凭证不出副机 / 零依赖
|
|
167
|
+
|
|
168
|
+
另修:架构图工具数、状态行(0.4.0 → 0.8.1)、测试数表、
|
|
169
|
+
`lib/` 目录表(补 `env-check.js`、`role: auto`)。
|
|
170
|
+
|
|
171
|
+
英文版 `README.en.md` 同步(同样的 4 处漂移 + 完整 Features 章节)。
|
|
172
|
+
|
|
173
|
+
#### 加了一条断言防复发(83)
|
|
174
|
+
|
|
175
|
+
工具数量这种「数字漂移」是最容易积累的文档债务 —— 每次加工具都忘了改
|
|
176
|
+
README,而读者会拿它当事实。所以新增断言:**数代码里 `reg(` + `fsOp(`
|
|
177
|
+
的注册调用数,与 README 里的声明比对**,两边任一变化都会立刻失败。
|
|
178
|
+
|
|
179
|
+
做了负向验证:临时把 README 的 12 改成 7,断言正确报错并给出
|
|
180
|
+
「实际 12 个(reg=8 + fsOp=4)vs 声明 7 个」。
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
packaging 4 项
|
|
184
|
+
doc-drift 118/118(+1)
|
|
185
|
+
diag:inject 11 项
|
|
186
|
+
tests 938(937 pass / 0 fail / 1 skip)
|
|
187
|
+
lint 干净
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 功能:工作区共享(SMB)—— 用本地 IDE 打开副机的文件
|
|
191
|
+
|
|
192
|
+
用户需求:想用本地 IDE 直接打开副机的项目目录。
|
|
193
|
+
|
|
194
|
+
实现方式:把副机的工作区通过 Windows SMB 共享出去,主机
|
|
195
|
+
`net use X: \\副机\dsh-work-...` 映射成盘符。
|
|
196
|
+
|
|
197
|
+
#### ⚠ 这是本插件**唯一主动放宽安全边界**的功能
|
|
198
|
+
|
|
199
|
+
开启后:
|
|
200
|
+
- `roots` / `denyPaths` 全部失效(Windows 内核直读,插件拦不住)
|
|
201
|
+
- 「清单先于字节」失效(中间产物、日志、`.env`、`.git` 密钥全部可见)
|
|
202
|
+
- 新增凭证面(主机需要副机的 Windows 账号)
|
|
203
|
+
|
|
204
|
+
所以按用户要求做了三重防护:
|
|
205
|
+
|
|
206
|
+
1. **二次确认**(硬要求)—— `POST /share` 缺 `confirm: 'yes'` 直接 400,
|
|
207
|
+
没有默认通过的分支;面板上必须勾选「我知道风险」按钮才可点
|
|
208
|
+
2. **只读 / 读写两选项**,默认**只读**(最小权限);读写用 `-ChangeAccess`
|
|
209
|
+
而非 `-FullAccess`(后者会把共享 ACL 也交出去,主机能改权限)
|
|
210
|
+
3. **不代执行** —— 只生成可复制的命令,执行由用户在管理员 PowerShell 里做
|
|
211
|
+
|
|
212
|
+
#### 为什么不代执行(重要设计决策)
|
|
213
|
+
|
|
214
|
+
`New-SmbShare` 需要管理员,而 DSH 通常以普通用户运行。Node 里提权的
|
|
215
|
+
失败表现(静默失败 / 被安全软件拦 / 权限半生效)比让用户手动跑一条命令
|
|
216
|
+
难排查得多。所以 `handleShareReq` 只做三件事:`probe`(探测管理员状态
|
|
217
|
+
与已有共享)、`plan`(生成共享 + 映射命令)、`unshare-plan`(生成卸载命令)。
|
|
218
|
+
|
|
219
|
+
#### 实现要点
|
|
220
|
+
|
|
221
|
+
- **协议**:新增 `SHARE_REQ` / `SHARE_RES` 帧,与 `fs/req` 同款 RPC 模式
|
|
222
|
+
- **E2E 强制**:`SHARE_REQ` 进 `E2E_REQUIRED_TYPES` —— 帧里含工作区
|
|
223
|
+
**绝对路径**(暴露副机目录结构),不许明文过中继
|
|
224
|
+
- **独立 pending 表**:`sharePending` 与 `fsPending` 刻意分开 —— 混用会让
|
|
225
|
+
超时归因变模糊(一条 share 超时被记成 fs-timeout,排查方向直接跑偏)
|
|
226
|
+
- **共享名稳定哈希**:同一目录反复开关共享名不变(不留孤儿共享),固定
|
|
227
|
+
`dsh-work-` 前缀便于识别与清理(不会误删用户的其它共享)
|
|
228
|
+
- **共享目录过 roots 边界**:`plan` 的目录必须经 `resolveWorkspace`
|
|
229
|
+
(realpath + roots 前缀 + denyPaths),与 fs 工具同一条规则
|
|
230
|
+
|
|
231
|
+
#### 文档
|
|
232
|
+
|
|
233
|
+
新增 [docs/workspace-share.md](docs/workspace-share.md):完整风险披露、
|
|
234
|
+
三步用法(面板开启 → 副机管理员执行 → 主机映射)、关闭方法、常见问题
|
|
235
|
+
(连不上怎么查、为什么共享名是怪名字、能不能共享整个盘、与 relay 的关系)。
|
|
236
|
+
|
|
237
|
+
#### 测试
|
|
238
|
+
|
|
239
|
+
- `test/share-service.test.js` **17 条**:共享名稳定性、只读/读写映射
|
|
240
|
+
(含"不得用 FullAccess")、单引号转义防注入、探测不抛、跨平台安全
|
|
241
|
+
- `test/share-integration.test.js` **12 条**:帧已声明、E2E 强制、
|
|
242
|
+
pending 表独立、断连 fail-fast、**二次确认是硬要求**、只读默认、
|
|
243
|
+
面板勾选框、不代执行、目录过 roots 边界
|
|
244
|
+
|
|
245
|
+
doc-drift 新增 81(接线完整)、82(安全约束)。
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
packaging 4 项
|
|
249
|
+
doc-drift 117/117
|
|
250
|
+
diag:inject 11 项
|
|
251
|
+
tests 926(925 pass / 0 fail / 1 skip)
|
|
252
|
+
lint 干净
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 开箱即用:装完就能用,不用读文档
|
|
256
|
+
|
|
257
|
+
用户标准(2026-10-17):"我希望是小白可用的,是开箱即用的"。
|
|
258
|
+
|
|
259
|
+
此前有三道门槛,每一道都能让小白卡死:
|
|
260
|
+
|
|
261
|
+
1. **`role` 默认 `''`** → 两台机器都要操作一次,不是开箱即用
|
|
262
|
+
2. **`webserver` 没绑 `0.0.0.0`** → 副机永远连不上,且**完全静默**
|
|
263
|
+
3. **副机表单 4 个字段** → 不知道节点 ID / 工作区填什么
|
|
264
|
+
|
|
265
|
+
#### 核心设计:`role` 默认 `auto`,按 `hubUrl` 推断
|
|
266
|
+
|
|
267
|
+
- 默认 `hub`:副机装完会以 hub 身份启动 —— 注册一个 nodeToken 为空、
|
|
268
|
+
不校验凭证的直连端点。**这是实打实的攻击面**(v0.7.0 之前的老问题)。
|
|
269
|
+
- 默认 `''`:两台机器都要操作一次,不是开箱即用。
|
|
270
|
+
- **默认 `auto`**:没配 `hubUrl` 就是主机(零操作),配了 `hubUrl` 就是副机
|
|
271
|
+
(贴完配对码零操作)。**两者都开箱即用,且不给副机开不该开的端点。**
|
|
272
|
+
|
|
273
|
+
判据只有一条,但它是副机与主机最本质的区别:`hubUrl` 是副机独有字段,
|
|
274
|
+
hub 侧根本不读它。配对码展开后必然产出 `hubUrl`,所以"贴码 = 自动变副机"。
|
|
275
|
+
|
|
276
|
+
推断结果带 `inferred` 标记(`no-hubUrl` / `hubUrl`),面板可显示依据。
|
|
277
|
+
显式写 `hub`/`worker`/`both` 永远优先于推断 —— 用户写的不该被猜掉。
|
|
278
|
+
|
|
279
|
+
#### 包内 patch 让 hub 真能被访问
|
|
280
|
+
|
|
281
|
+
```yaml
|
|
282
|
+
- id: webserver
|
|
283
|
+
config:
|
|
284
|
+
host: '0.0.0.0'
|
|
285
|
+
port: 19387
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
不覆盖这一行,hub 只监听 loopback → 副机永远连不上。这是"能不能用"的决定性一条。
|
|
289
|
+
|
|
290
|
+
**安全披露**(写进 patch 注释 + README + 面板 + 体检):绑 `0.0.0.0` 意味着
|
|
291
|
+
**DSH 的整个 Web GUI 也能被局域网访问**,不只是本插件的节点端点。DSH 自带
|
|
292
|
+
Host/Origin 围栏 + cookie 鉴权,但暴露面确实变大了。用户可在 profile 里覆盖
|
|
293
|
+
回 `127.0.0.1`(此时请改用 relay 模式)。
|
|
294
|
+
|
|
295
|
+
优先级安全:`cordis.yml` 头注释说明应用顺序是 bundles → profile patch →
|
|
296
|
+
--patch overlays,所以**用户 profile 里的同名行会覆盖包内默认**。
|
|
297
|
+
|
|
298
|
+
#### 副机表单简化
|
|
299
|
+
|
|
300
|
+
- **工作区预填**(Windows `D:\work`,其他 `~/work`)—— 此前是空,小白不知道
|
|
301
|
+
填什么:填相对路径被拒、填 `C:\` 太宽(安全边界)、填不存在的目录任务全报
|
|
302
|
+
`workspace-denied`
|
|
303
|
+
- **节点 ID 折叠为高级选项** —— 默认取主机名,90% 用户不用改
|
|
304
|
+
- **目录自动创建** —— 写入配置时 `mkdirSync(recursive)` 建不存在的 roots
|
|
305
|
+
(只建不改;相对路径跳过交给 resolveRoots 去拒;失败不阻塞写配置,
|
|
306
|
+
结果进 `warnings` 随响应回给面板)
|
|
307
|
+
|
|
308
|
+
#### 首次自动体检
|
|
309
|
+
|
|
310
|
+
面板加载时**自动跑一次**体检(`autoChecked` ref 保证只跑一次,不进 4 秒轮询)。
|
|
311
|
+
此前要用户主动点「体检」按钮 —— 但小白不知道要点。
|
|
312
|
+
|
|
313
|
+
#### 文档修正
|
|
314
|
+
|
|
315
|
+
README 安装段重写。顺带修正一个**长期错误建议**:旧文档教用户写
|
|
316
|
+
`inject: []`,那是错的 —— 副机 agent 的 ctx 继承插件的依赖声明,
|
|
317
|
+
`inject: []` 会让 agent 没有任何 shell/文件工具,任务"跑完了但什么都没干"。
|
|
318
|
+
正确写法是 `['tools', 'agentLoop', 'sessions']`。doc-drift 断言 30 也跟着改。
|
|
319
|
+
|
|
320
|
+
#### 测试
|
|
321
|
+
|
|
322
|
+
`test/out-of-box.test.js` **15 条**:auto 推断的四种情形(没 hubUrl / 有 hubUrl /
|
|
323
|
+
空白 hubUrl / 显式 role 优先)、空 role 仍走引导、非法 role 不回退 hub、
|
|
324
|
+
包内 patch 绑 0.0.0.0 + 安全披露、表单预填/折叠、自动建目录、首次自动体检。
|
|
325
|
+
|
|
326
|
+
doc-drift 新增 79(auto + webserver 绑定)、80(表单简化 + 自动建目录 + 自动体检)。
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
packaging 4 项
|
|
330
|
+
doc-drift 115/115
|
|
331
|
+
diag:inject 11 项
|
|
332
|
+
tests 897(896 pass / 0 fail / 1 skip)
|
|
333
|
+
lint 干净
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 功能:环境体检(命令行为准,面板可见)
|
|
337
|
+
|
|
338
|
+
这个插件最贵的失败**全是静默的**:
|
|
339
|
+
|
|
340
|
+
- **webserver 绑 loopback** → 副机永远连不上,而主机侧毫无异常
|
|
341
|
+
(hub healthz 200、面板能开、工具都在)
|
|
342
|
+
- **inject 缺服务** → hub 半死(路由活着但 8 个工具全丢)
|
|
343
|
+
- **roots 不存在** → 任务全被 `workspace-denied` 拒绝
|
|
344
|
+
|
|
345
|
+
用户只能猜"地址错了?token 错了?防火墙?"。
|
|
346
|
+
|
|
347
|
+
本版新增 `lib/env-check.js`,命令行与面板**共用同一份逻辑**:
|
|
348
|
+
|
|
349
|
+
- `envChecks()` 逐项检查:角色已配置 / inject 含必需服务 / **Web 服务绑在
|
|
350
|
+
可达地址** / 端口可用 / 防火墙 / roots 存在 / Tailscale / 副机在线
|
|
351
|
+
- 每项都带 **detail(为什么)** 与 **fix(怎么修)** —— 不是干巴巴一个 ✗
|
|
352
|
+
- `autoFix` 标记"我们能自动改配置修好它"
|
|
353
|
+
|
|
354
|
+
三档处置(产品决策):
|
|
355
|
+
|
|
356
|
+
| 档位 | 做法 | 例子 |
|
|
357
|
+
|---|---|---|
|
|
358
|
+
| A 自动修 | 只改我们自己的配置,不下载任何东西 | webserver 绑 0.0.0.0、role/inject/roots |
|
|
359
|
+
| B 提示 + 命令 | 给官方链接与命令,**不代下载** | Tailscale 未安装 |
|
|
360
|
+
| C 只诊断 | 报清楚,让人判断 | 端口占用、防火墙、模型凭证 |
|
|
361
|
+
|
|
362
|
+
**为什么不代下载**(边界):零 npm 依赖 / 零构建是核心卖点。代用户下载第三方
|
|
363
|
+
软件会让本插件变成软件分发方 —— 装哪个版本、从哪下、校验和怎么保证、装失败
|
|
364
|
+
算谁的、供应链攻击面,全落在我们头上。所以档位 B 只给链接与命令。
|
|
365
|
+
|
|
366
|
+
配套:
|
|
367
|
+
|
|
368
|
+
- `applyHub` 新增**可达性自检**:调 `webServer.getOrigin()` 判定是否绑在
|
|
369
|
+
loopback,是则 `log('warn')` 带修法。自检本身包 try/catch ——
|
|
370
|
+
`getOrigin()` 在 listen 前会抛,**绝不能因为自检让 hub 起不来**
|
|
371
|
+
- 面板新增**红色横幅**(绑 loopback 时)+「体检」按钮 + 体检结果区
|
|
372
|
+
- 新增 `GET /api/remote-agent/ui/env-check` 路由
|
|
373
|
+
|
|
374
|
+
测试:`test/env-check.test.js` **24 条**(loopback 识别的 8 种写法 +
|
|
375
|
+
各检查项的通过与失败 + 修法必须具体 + warn 不计入 failed +
|
|
376
|
+
config 传 null 不抛 + 渲染每条 ✗ 都带修法)。
|
|
377
|
+
doc-drift 新增 77(三处齐备)、78(覆盖静默失败项 + 不代下载边界)。
|
|
378
|
+
|
|
379
|
+
写测试时抓到 3 个真问题:`config: null` 会 TypeError、Tailscale 文案漏了
|
|
380
|
+
"不代下载"说明、以及一个断言的正则转义层数写错。
|
|
381
|
+
|
|
12
382
|
## [0.8.0] - 2026-10-17
|
|
13
383
|
|
|
14
384
|
### 功能:#27 完整版 —— 模型清单自述 + 面板下拉
|
package/README.en.md
CHANGED
|
@@ -18,7 +18,7 @@ explicitly pull them.
|
|
|
18
18
|
┌──────────── main machine (hub) ────────────┐
|
|
19
19
|
│ DSH · model · session logs · all credentials │
|
|
20
20
|
│ ┌─ dsh-remote-agent ─────────────────────┐ │
|
|
21
|
-
│ │
|
|
21
|
+
│ │ 12 remote_* tools + Web panel │ │
|
|
22
22
|
│ └────────────────────────────────────────┘ │
|
|
23
23
|
└──────────────────▲──────────────────────────┘
|
|
24
24
|
│ node dials in (direct / relay)
|
|
@@ -32,15 +32,21 @@ explicitly pull them.
|
|
|
32
32
|
|
|
33
33
|
| | |
|
|
34
34
|
|---|---|
|
|
35
|
-
| **Status** | 0.
|
|
35
|
+
| **Status** | 0.8.1 —— hub + worker end-to-end usable; both `exec` and `agent` paths verified on two real machines; CI covers Windows / macOS / Linux |
|
|
36
36
|
| **DSH** | Desktop / CLI, verified on 0.2.0-rc.2 |
|
|
37
37
|
| **Platforms** | Windows (fully verified) · Linux / macOS (CI covered) |
|
|
38
38
|
| **Dependencies** | **Zero external npm dependencies** —— `node:` built-ins only |
|
|
39
39
|
| **Node** | `^22.19.0 \|\| >=24.0.0` |
|
|
40
|
+
| **Tests** | 938 (937 pass / 1 skip) · 117 doc-drift assertions · eslint strict |
|
|
40
41
|
|
|
41
42
|
**Web panel**(sidebar → "远程同事" / Remote co-worker):node list with CPU /
|
|
42
43
|
memory badges, task dispatch, task states, result summaries, workspace changes,
|
|
43
|
-
change diffs (unified diffs of text-file edits), final artifacts, log tail
|
|
44
|
+
change diffs (unified diffs of text-file edits), final artifacts, log tail,
|
|
45
|
+
**environment health-check**, **workspace sharing (SMB)**, model dropdown.
|
|
46
|
+
|
|
47
|
+
> **Works out of the box**: restart DSH after install and it just works. The
|
|
48
|
+
> bundled patch defaults `role: auto` (no `hubUrl` ⇒ hub) and binds `webserver`
|
|
49
|
+
> to `0.0.0.0:19387` (otherwise workers can never connect). See *Install* below.
|
|
44
50
|
|
|
45
51
|
<p align="center">
|
|
46
52
|
<img src="docs/screenshots/panel-main.png" width="720" alt="Web panel: nodes, dispatch form, task list">
|
|
@@ -106,6 +112,91 @@ Not interactive? One command does it all: `--role worker --hub-url
|
|
|
106
112
|
|
|
107
113
|
---
|
|
108
114
|
|
|
115
|
+
## Features
|
|
116
|
+
|
|
117
|
+
### 1. Dispatch tasks to the worker
|
|
118
|
+
|
|
119
|
+
| Feature | What it does |
|
|
120
|
+
|---|---|
|
|
121
|
+
| **Two task modes** | `exec` runs a command directly (`npm run build`, `git pull`, bulk renames in `D:\work`); `agent` spins up an **independent agent session** on the worker that plans, calls tools and writes files itself |
|
|
122
|
+
| **Two-phase approval** | `exec` is held for confirmation by default (`approval: 'manual'`) — nothing runs until a human approves. Guards against an AI dispatching a destructive command |
|
|
123
|
+
| **Dry-run preview** | See which node it routes to, which model, which directory, what artifacts to expect — without dispatching |
|
|
124
|
+
| **Repo-aware routing** | Workers report git repos under their `roots`; `remote_task({repo: 'my-project'})` finds the worker that has it |
|
|
125
|
+
| **Load-aware routing** | Workers report CPU/memory/load every 30s; omitting `node` picks the least loaded |
|
|
126
|
+
| **Model selection** | Panel offers a dropdown of the worker's **self-reported model list** (no guessing provider/model names); headless backends that can't take a model are **rejected before dispatch** |
|
|
127
|
+
| **Per-mode timeouts** | `exec` defaults to 10 minutes (a runaway command shouldn't hold a machine for an hour); `agent` defaults to 1 hour |
|
|
128
|
+
| **Concurrency cap** | Worker `maxConcurrent` (default 4); excess tasks queue, `remote_status` shows queue depth and oldest wait |
|
|
129
|
+
|
|
130
|
+
### 2. Get results without moving the whole workspace
|
|
131
|
+
|
|
132
|
+
| Feature | What it does |
|
|
133
|
+
|---|---|
|
|
134
|
+
| **Handoff card** | A task returns one card: summary + workspace changes + artifact manifest + exit code. **Not** the whole directory |
|
|
135
|
+
| **Three-way artifact classification** | Files are `final` / `intermediate` / `other`; **only `final` can be pulled**. Logs, caches, `node_modules` stay on the worker |
|
|
136
|
+
| **Explicit pull** | Pull individual files with `remote_pull`, glob filters supported |
|
|
137
|
+
| **Resume on interruption** | A pull that drops mid-way doesn't restart: received bytes are kept and the retry continues from `startOffset` with `prefixSha256` verification |
|
|
138
|
+
| **Change diff view** | Compares the workspace before/after, showing exactly which files changed and by how many bytes |
|
|
139
|
+
|
|
140
|
+
### 3. Browse the worker's files directly (without touching the task queue)
|
|
141
|
+
|
|
142
|
+
Four **read-only** tools over a separate `fs/req`↔`fs/res` frame — **they bypass the
|
|
143
|
+
task queue and don't consume a concurrency slot**. Even when the worker is saturated
|
|
144
|
+
with a long task, these answer instantly:
|
|
145
|
+
|
|
146
|
+
| Tool | Purpose |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `remote_ls` | List a directory (single level, paginated) |
|
|
149
|
+
| `remote_read` | Read a file (text by line range; binaries return metadata only) |
|
|
150
|
+
| `remote_grep` | Full-text search **locally** on the worker (pure JS, no process spawn) |
|
|
151
|
+
| `remote_tree` | Directory tree summary (scanning happens on the worker; only structure returns) |
|
|
152
|
+
|
|
153
|
+
Safety: paths must be inside the node's `roots`, `denyPaths` applies, symlinks are
|
|
154
|
+
listed but never dereferenced.
|
|
155
|
+
|
|
156
|
+
### 4. Open the worker's files in your local IDE (SMB share)
|
|
157
|
+
|
|
158
|
+
The panel's *Workspace sharing* section publishes a worker directory over SMB; the
|
|
159
|
+
hub maps it with `net use X: \\worker\dsh-work-...` and you can open it in local VS Code.
|
|
160
|
+
|
|
161
|
+
> **⚠ This bypasses the plugin's security boundary** (`roots` / `denyPaths` stop
|
|
162
|
+
> applying, intermediates and secrets become visible to the hub, and a new
|
|
163
|
+
> credential surface appears). Requires **explicit second confirmation**; **read-only
|
|
164
|
+
> by default**. Full risk notes: [docs/workspace-share.md](docs/workspace-share.md).
|
|
165
|
+
|
|
166
|
+
### 5. Diagnose problems yourself
|
|
167
|
+
|
|
168
|
+
| Feature | What it does |
|
|
169
|
+
|---|---|
|
|
170
|
+
| **Environment health-check** | One click in the panel checks 8 items: role config, `inject` dependencies, **whether the web server is bound to a reachable address**, port, firewall, `roots` existence, Tailscale, workers online. **Every item comes with a concrete fix**; auto-fixable ones are flagged |
|
|
171
|
+
| **Reachability self-check** | At startup, verifies `webServer` isn't bound to loopback — if it is, workers can never connect while the hub side looks perfectly healthy. Logs a clear warning and shows a red banner |
|
|
172
|
+
| **Queue health snapshot** | Workers report `queued` / `running` / `maxConcurrent` / oldest wait with each resource frame; both the panel and `remote_nodes` warn when the queue is jammed |
|
|
173
|
+
| **"Running" grouping** | Running tasks are pinned on top with **elapsed time** (the only clue for "is it stuck?") |
|
|
174
|
+
| **Cancel really aborts** | `remote_cancel` actually terminates the agent loop (not just setting a flag and letting it finish) |
|
|
175
|
+
| **Audit log** | Dispatch / cancel / pull / approval all recorded (`~/.dsh-remote-agent/audit.jsonl`, rotated) |
|
|
176
|
+
|
|
177
|
+
### 6. Disconnects, NAT traversal, security
|
|
178
|
+
|
|
179
|
+
| Feature | What it does |
|
|
180
|
+
|---|---|
|
|
181
|
+
| **Disconnects don't kill tasks** | Relay down or hub restarted, the worker keeps running; `task/sync` reconciles on reconnect and **never re-runs completed work** |
|
|
182
|
+
| **Two transports** | `direct` (same LAN / Tailscale); `relay` (both behind NAT, needs a VPS) |
|
|
183
|
+
| **End-to-end encryption** | In relay mode, business frames use X25519 + AES-256-GCM; the relay only sees routing envelopes |
|
|
184
|
+
| **Hub authentication** | Workers verify the hub: persistent ed25519 identity + fingerprint carried in the pairing code, TOFU-pinned on first connect |
|
|
185
|
+
| **Credentials never leave the worker** | The protocol carries no model keys; the worker runs its own loop with its own model config |
|
|
186
|
+
| **Zero npm dependencies** | `node:` built-ins only, no build step, `lib/` is the artifact |
|
|
187
|
+
|
|
188
|
+
### Tool reference (12 tools)
|
|
189
|
+
|
|
190
|
+
| Category | Tools |
|
|
191
|
+
|---|---|
|
|
192
|
+
| **Nodes** | `remote_nodes` (list + load + queue + model list) |
|
|
193
|
+
| **Tasks** | `remote_task` (dispatch) · `remote_status` (progress) · `remote_cancel` (abort) |
|
|
194
|
+
| **Approval** | `approval: 'manual'` holds the task → approve/reject from the panel or tools |
|
|
195
|
+
| **Results** | `remote_handoff` (result card) · `remote_manifest` (artifact list) · `remote_pull` (fetch finals) · `remote_purge` (cleanup) |
|
|
196
|
+
| **Files** | `remote_ls` · `remote_read` · `remote_grep` · `remote_tree` |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
109
200
|
## Core invariants
|
|
110
201
|
|
|
111
202
|
These are the design premises of this package — read them before changing it:
|
|
@@ -335,7 +426,7 @@ computer.** Read **[SECURITY.md](SECURITY.md)** before deploying — especially:
|
|
|
335
426
|
| Path | Purpose |
|
|
336
427
|
|---|---|
|
|
337
428
|
| `lib/index.js` | Plugin entry (role dispatcher: `role: hub` / `worker` / `both`) |
|
|
338
|
-
| `lib/hub/**` | Hub side: node registry,
|
|
429
|
+
| `lib/hub/**` | Hub side: node registry, **12 tools**, subagent provider, UI routes, environment health-check |
|
|
339
430
|
| `lib/hub/views.js` | **Shared projection layer** —— tools and the UI use the same field whitelist |
|
|
340
431
|
| `lib/node/**` | Worker side: dial-in, task engine, artifact service, exec/agent backends |
|
|
341
432
|
| `lib/client.js` | Browser half (Web panel) |
|