dsh-remote-agent 0.0.0-stage → 0.4.1

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +700 -0
  2. package/CONTRIBUTING.md +358 -0
  3. package/KNOWN-ISSUES.md +601 -0
  4. package/LICENSE +21 -0
  5. package/README.en.md +353 -0
  6. package/README.md +317 -2
  7. package/SECURITY.md +348 -0
  8. package/bin/dsh-remote-relay.js +257 -0
  9. package/bin/dsh-remote-setup.js +440 -0
  10. package/bin/setup.cmd +30 -0
  11. package/cordis.patch.yml +87 -0
  12. package/deploy/relay-vps/Caddyfile +24 -0
  13. package/deploy/relay-vps/README.md +132 -0
  14. package/deploy/relay-vps/dsh-remote-relay.service +47 -0
  15. package/deploy/relay-vps/nginx.conf +49 -0
  16. package/deploy/relay-vps/relay.env.example +20 -0
  17. package/docs/dispatch-automation-eval.md +140 -0
  18. package/docs/screenshots/panel-main.png +0 -0
  19. package/docs/screenshots/panel-task-detail.png +0 -0
  20. package/docs/verification-agent-e2e.md +425 -0
  21. package/docs/verification-repos-routing-real.md +521 -0
  22. package/docs/verification-resume-real.md +254 -0
  23. package/docs/worker-setup-pitfalls.md +266 -0
  24. package/lib/artifacts.js +451 -0
  25. package/lib/client.js +1752 -0
  26. package/lib/e2e.js +304 -0
  27. package/lib/glob.js +225 -0
  28. package/lib/hub/audit-log.js +161 -0
  29. package/lib/hub/hub-util.js +114 -0
  30. package/lib/hub/index.js +500 -0
  31. package/lib/hub/node-registry.js +943 -0
  32. package/lib/hub/pull-manager.js +781 -0
  33. package/lib/hub/registry-handshake.js +403 -0
  34. package/lib/hub/registry-ingest.js +346 -0
  35. package/lib/hub/relay-link.js +228 -0
  36. package/lib/hub/subagent-provider.js +424 -0
  37. package/lib/hub/templates.js +693 -0
  38. package/lib/hub/tools.js +1059 -0
  39. package/lib/hub/ui-routes.js +856 -0
  40. package/lib/hub/views.js +351 -0
  41. package/lib/identity.js +271 -0
  42. package/lib/index.js +272 -0
  43. package/lib/node/artifact-service.js +231 -0
  44. package/lib/node/diff.js +124 -0
  45. package/lib/node/exec.js +536 -0
  46. package/lib/node/hub-pin.js +71 -0
  47. package/lib/node/index.js +1022 -0
  48. package/lib/node/repos.js +134 -0
  49. package/lib/node/resource.js +63 -0
  50. package/lib/node/runner.js +792 -0
  51. package/lib/node/task-engine.js +712 -0
  52. package/lib/node/task-store.js +273 -0
  53. package/lib/node/transport.js +763 -0
  54. package/lib/pairing.js +186 -0
  55. package/lib/protocol.js +414 -0
  56. package/lib/relay-config.js +317 -0
  57. package/lib/relay.js +732 -0
  58. package/lib/setup-routes.js +313 -0
  59. package/lib/setup.js +522 -0
  60. package/lib/ws.js +466 -0
  61. package/package.json +103 -4
  62. package/profiles/README.md +573 -0
  63. package/profiles/hub.cordis.yml +84 -0
  64. package/profiles/worker.cordis.yml +60 -0
  65. package/scripts/check-doc-drift.mjs +802 -0
  66. package/scripts/check-packaging.mjs +177 -0
  67. package/scripts/diag-inject.mjs +243 -0
  68. package/scripts/e2e-live-check.mjs +181 -0
  69. package/scripts/relay-init.ps1 +286 -0
  70. package/scripts/relay-init.sh +199 -0
package/README.en.md ADDED
@@ -0,0 +1,353 @@
1
+ # dsh-remote-agent
2
+
3
+ [![CI](https://github.com/zbzbhs/dsh-remote-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/zbzbhs/dsh-remote-agent/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ ![Node](https://img.shields.io/badge/node-%E2%89%A522.19%20%7C%7C%20%3E%3D24-green)
6
+ ![dependencies](https://img.shields.io/badge/dependencies-zero-brightgreen)
7
+
8
+ [简体中文](README.md) | **English**
9
+
10
+ Turn another computer into a **remote agent co-worker** for your DSH.
11
+
12
+ The worker machine runs its own DSH with an independent agent loop — tasks keep
13
+ running locally on it. Your main machine only receives the **handoff result
14
+ card** and the **artifact manifest**; final artifacts cross over only when you
15
+ explicitly pull them.
16
+
17
+ ```
18
+ ┌──────────── main machine (hub) ────────────┐
19
+ │ DSH · model · session logs · all credentials │
20
+ │ ┌─ dsh-remote-agent ─────────────────────┐ │
21
+ │ │ 7 remote_* tools + Web panel │ │
22
+ │ └────────────────────────────────────────┘ │
23
+ └──────────────────▲──────────────────────────┘
24
+ │ node dials in (direct / relay)
25
+ ┌──────────────────┴──────────────────────────┐
26
+ │ ┌─ worker machine ─────────────────────────┐ │
27
+ │ │ independent agent loop · own model ctx │ │
28
+ │ │ D:\work\** intermediates stay here │ │
29
+ │ └──────────────────────────────────────────┘ │
30
+ └─────────────────────────────────────────────┘
31
+ ```
32
+
33
+ | | |
34
+ |---|---|
35
+ | **Status** | 0.4.0 —— hub side end-to-end usable; real-runtime dial-in verified (standalone worker process → running DSH desktop hub, see [KNOWN-ISSUES §8](KNOWN-ISSUES.md)); both `exec` and `agent` paths verified on two real machines |
36
+ | **DSH** | Desktop / CLI, verified on 0.2.0-rc.2 |
37
+ | **Platforms** | Windows (fully verified) · Linux / macOS (CI covered) |
38
+ | **Dependencies** | **Zero external npm dependencies** —— `node:` built-ins only |
39
+ | **Node** | `^22.19.0 \|\| >=24.0.0` |
40
+
41
+ **Web panel**(sidebar → "远程同事" / Remote co-worker):node list with CPU /
42
+ memory badges, task dispatch, task states, result summaries, workspace changes,
43
+ change diffs (unified diffs of text-file edits), final artifacts, log tail.
44
+
45
+ <p align="center">
46
+ <img src="docs/screenshots/panel-main.png" width="720" alt="Web panel: nodes, dispatch form, task list">
47
+ </p>
48
+
49
+ <p align="center">
50
+ <img src="docs/screenshots/panel-task-detail.png" width="720" alt="Task detail: summary, workspace changes, final artifacts">
51
+ </p>
52
+
53
+ ---
54
+
55
+ ## Up and running in five minutes
56
+
57
+ Pick one route based on how comfortable you are with a terminal — **every route
58
+ leads to the same endpoint**:
59
+
60
+ | Route | Hub | Worker | For |
61
+ |---|---|---|---|
62
+ | **① Zero terminal**(Windows) | Double-click `bin/setup.cmd` → Enter → Web panel "副机接入" copies the pairing code | Double-click the same file → paste the pairing code → Enter writes config | Anyone who flinches at a black window |
63
+ | **② Wizard**(recommended) | Run the wizard in a terminal → Enter to confirm | Same, paste the pairing code | You can open a terminal |
64
+ | **③ Manual** | Hand-write config per the section below | Hand-write (`inject: []` is mandatory) | You want full control |
65
+
66
+ After any route: **restart DSH on both machines**, then ask the hub
67
+ "which nodes are online on the worker?" —— if it answers, you're connected.
68
+ When something breaks, run `--doctor`; every ✗ item ships with a fix.
69
+
70
+ ```bash
71
+ node bin/dsh-remote-setup.js
72
+ ```
73
+
74
+ > **Windows, no terminal?** In the plugin's `bin/` directory **double-click
75
+ > `setup.cmd`** —— then just keep pressing Enter in the window
76
+ > that opens.
77
+ > The plugin lives at `%USERPROFILE%\.dsh\dsh_orb\dsh-plugin\dsh-remote-agent\bin\`.
78
+
79
+ 1. **Run on the hub**: choose "1 = hub" → the wizard auto-detects the LAN IP,
80
+ generates a strong-random `nt_` `nodeToken`, and prints the **worker
81
+ pairing code** (one line, `dshra1://…`) plus complete YAML for both machines
82
+ (including the **firewall allow command**).
83
+ 2. **Run on the worker**: choose "2 = worker" → **paste the pairing code the
84
+ hub gave you** (or type the address and token by hand) → the wizard
85
+ **probes the hub port first**, then writes the worker config. The
86
+ error-prone `inject: []` is filled in for you (without it the worker stays
87
+ silent forever and never comes online). The worker config may also be just
88
+ one line `pair: '<pairing code>'`, expanded to address + token at runtime
89
+ (explicit config wins; writing `hubUrl:` overrides it temporarily).
90
+ 3. Restart DSH on both machines, ask the hub "**which nodes are online on the
91
+ worker?**" —— an answer means you're connected.
92
+ 4. **Terminal-free path (GUI)**: open the hub's Web panel → the "**副机接入**"
93
+ (worker onboarding) button → generates and copies the pairing code in one
94
+ click (firewall command included). When dispatching from the hub panel you
95
+ can also set the model the worker should use in the "模型" (model) field
96
+ (leave empty for the worker's default).
97
+ 5. When in doubt, run the doctor (reads your config files, checks each item,
98
+ every ✗ comes with a fix):
99
+
100
+ ```bash
101
+ node bin/dsh-remote-setup.js --doctor your-config.yml
102
+ ```
103
+
104
+ Not interactive? One command does it all: `--role worker --hub-url
105
+ 192.168.1.10:19387 --token nt_xxx --roots D:\work --node-id slave-pc`.
106
+
107
+ ---
108
+
109
+ ## Core invariants
110
+
111
+ These are the design premises of this package — read them before changing it:
112
+
113
+ | Invariant | Meaning |
114
+ |---|---|
115
+ | **Credentials never leave the worker** | The protocol carries no model keys; the worker uses its own model config |
116
+ | **Disconnects don't kill tasks** | Tasks keep running locally on the worker; even a relay outage finishes, and reconciliation happens on reconnect |
117
+ | **The relay sees only ciphertext** | In relay mode, business frames are end-to-end encrypted (X25519 + AES-256-GCM, see [SECURITY.md](SECURITY.md)): the relay only sees the routing envelope (`t`/`id`/`to`/`from`); goals and artifact contents are all ciphertext |
118
+ | **Manifest before bytes** | On completion only the manifest + handoff come back; artifacts transfer only via `remote_pull` |
119
+ | **Only `role:final` crosses the wire** | Logs / caches / intermediate build files stay on the worker; only metadata is reported |
120
+ | **Resumable transfer** | If `remote_pull` drops mid-transfer, it does not restart from zero: received bytes are kept, retries resume from the offset, full sha256 verification still applies |
121
+
122
+ ---
123
+
124
+ ## Install
125
+
126
+ ### From npm or GitHub
127
+
128
+ ```bash
129
+ # via DSH's plugin installer (GitHub)
130
+ dsh plugin add github:zbzbhs/dsh-remote-agent
131
+
132
+ # or via npm
133
+ npm install -g dsh-remote-agent
134
+ ```
135
+
136
+ Or type `github:zbzbhs/dsh-remote-agent` into DSH's plugin dialog.
137
+
138
+ After installing, make sure this stanza exists in `cordis.patch.yml` (the
139
+ in-package patch inserts it automatically):
140
+
141
+ ```yaml
142
+ - id: remote-agent
143
+ name: dsh-remote-agent
144
+ inject: ['webServer', 'tools', 'systemPrompt']
145
+ config:
146
+ role: hub
147
+ ```
148
+
149
+ ### Worker
150
+
151
+ **Same package, different config** —— the key point is that **`inject: []` is
152
+ mandatory**:
153
+
154
+ ```yaml
155
+ - id: remote-agent
156
+ name: dsh-remote-agent
157
+ inject: [] # ← mandatory on the worker, or it never activates (see below)
158
+ config:
159
+ role: worker
160
+ nodeId: slave-pc
161
+ transportMode: direct
162
+ hubUrl: 'ws://<hub-host>:19387/api/remote-agent/node'
163
+ token: '<same as the hub's nodeToken, or leave empty>'
164
+ roots: ['D:\work'] # ← change to a real directory on the worker
165
+ ```
166
+
167
+ **Typing addresses is error-prone? Use the pairing code** —— the hub wizard
168
+ prints one line `dshra1://…` (address + token inside). On the worker, replace
169
+ the `transportMode/hubUrl/token` lines with a single line:
170
+
171
+ ```yaml
172
+ config:
173
+ role: worker
174
+ pair: 'dshra1://eyJ2IjoxLC…' # ← the full line printed by the hub wizard
175
+ roots: ['D:\work']
176
+ ```
177
+
178
+ > **Two rules about the pairing code**
179
+ >
180
+ > 1. **It contains the token, so it IS a credential** —— hand it over in
181
+ > person or via encrypted chat; never paste it into public channels.
182
+ > 2. **`roots` is not part of the pairing code** —— which directories the
183
+ > worker may touch is the worker's own security boundary, always declared
184
+ > by the worker config; nothing on the hub side can decide it.
185
+ >
186
+ > Explicit config wins: when `pair` and `hubUrl` both exist, `hubUrl` applies —
187
+ > handy for temporarily switching addresses while debugging. A broken pairing
188
+ > code never prevents the worker from starting —— it logs one warning and is
189
+ > treated as unconfigured (`--doctor` will flag it).
190
+
191
+ > **Hub identity verification (on by default)**: the worker verifies the hub
192
+ > too. On first start the hub generates a persistent identity (fingerprint
193
+ > `hubfp1-…`, stored at `~/.dsh-remote-agent/hub-identity.json`); the pairing
194
+ > code carries the fingerprint automatically —— pasting it pins the hub on
195
+ > first connect (TOFU, state in the worker's
196
+ > `~/.dsh-remote-agent/hub-pin.json`). Manual config can pin explicitly with
197
+ > `pinnedHubFingerprint: hubfp1-…`; from then on a fingerprint mismatch means
198
+ > the worker refuses to come online and stops reconnecting (if you deliberately
199
+ > swapped hubs, delete `hub-pin.json` and restart to re-pin).
200
+ > See [SECURITY.md](SECURITY.md) checklist item 7.
201
+
202
+ > **Why the worker needs `inject: []`**
203
+ >
204
+ > The in-package `cordis.patch.yml` declares
205
+ > `inject: ['webServer','tools','systemPrompt']` —— those are the services the
206
+ > **hub** needs. The worker profile doesn't have them, so without clearing the
207
+ > line it would sit in **PENDING** and **never activate**, and the worker would
208
+ > never dial in. The loader overrides by id, so writing `inject: []` overrides
209
+ > the in-package declaration.
210
+
211
+ **Connectivity paths**(both behind NAT / one side has a public IP / tunnels)
212
+ are covered in **[profiles/README.md](profiles/README.md)** —— it has config
213
+ snippets and verification commands for every path, plus a **fallback chain**:
214
+ direct when possible, otherwise fall back to a relay VPS or a cloudflared
215
+ tunnel; if the whole chain is down the worker keeps running locally and
216
+ reconciles on reconnect. The relay VPS deployment template (TLS-ready) lives in
217
+ [deploy/relay-vps/](deploy/relay-vps/). In relay mode business frames travel
218
+ **end-to-end encrypted** (the relay sees only ciphertext; plaintext goals and
219
+ artifacts never cross it; the handshake is initiated by the worker and falls
220
+ back to plaintext compatibility after a 5 s timeout with legacy relays).
221
+ Security boundaries and known limitations: [SECURITY.md](SECURITY.md).
222
+
223
+ ---
224
+
225
+ ## Usage
226
+
227
+ Once installed you **don't need to memorize commands** — just talk:
228
+
229
+ | You say | The agent calls |
230
+ |---|---|
231
+ | "Which nodes are online on the worker?" | `remote_nodes` |
232
+ | "Have the worker run the tests in D:\work" | `remote_task` |
233
+ | "How is that task going?" | `remote_status` |
234
+ | "Give me a summary" | `remote_handoff` |
235
+ | "What artifacts are there?" | `remote_manifest` |
236
+ | "Pull report.pdf over" | `remote_pull` |
237
+ | "Cancel that task" | `remote_cancel` |
238
+
239
+ There is also a Web panel (sidebar → "远程同事"): node list (with CPU / memory
240
+ / load badges), task dispatch, task states, result summaries, workspace
241
+ changes, change diffs, final artifacts, log tail.
242
+
243
+ ### Tool reference
244
+
245
+ <details>
246
+ <summary><code>remote_task</code> parameters</summary>
247
+
248
+ | Parameter | Meaning |
249
+ |---|---|
250
+ | `goal` | Task brief. In `mode=agent` this is the task statement for the worker's agent; in `mode=exec` it is the command to run |
251
+ | `node` | Target node id. When omitted, auto-routed: with multiple nodes the least-loaded one wins (running tasks → CPU → memory; agent mode only considers agent-capable nodes) |
252
+ | `repo` | Route by repository: pass a repo name, remote-URL fragment, or local path fragment — the task goes to a node that holds that repo (load-aware when several do). When `workspace` is omitted, the repo's local path is used automatically. Workers scan git repos under their `roots` after connecting (zero subprocess, credentials stripped locally, refreshed every 10 minutes) |
253
+ | `approval` | Two-phase approval: `manual` = hub holds the task until approved in the panel (auto-rejected on timeout, fail-closed); `auto` = dispatch immediately. Defaults: **exec → manual, agent → auto** |
254
+ | `dryRun` | Preview only: returns which node/workspace/mode would be used, computed by the exact same routing + validation as a real dispatch |
255
+ | `workspace` | Absolute path on the worker, must be inside the node's `roots`. Defaults when omitted |
256
+ | `mode` | `agent` (worker starts an independent agent session, default) / `exec` (run a command directly) |
257
+ | `model` | Optional. Model the worker should use (`mode=agent` only): `{model:"deepseek-chat"}` or `{provider:"openai",model:"gpt-4o"}`. Selection only, no credentials —— the worker runs it with its own provider config; unsupported on headless backends |
258
+ | `outputs` | Glob of expected outputs (relative to the workspace), used to filter the artifact manifest |
259
+ | `timeoutMs` | Worker-side timeout, 1 hour by default |
260
+ | `denyPaths` | Extra absolute paths to deny |
261
+ | `artifactRules` | Override built-in artifact role classification, e.g. `{ intermediate: ["build/**"] }` |
262
+
263
+ **`exec` mode runs PowerShell on Windows workers** —— separate commands with
264
+ `;`, not `&` (rejected). For cmd syntax, use `cmd /c "..."` explicitly.
265
+
266
+ </details>
267
+
268
+ ---
269
+
270
+ ## Security
271
+
272
+ **This plugin lets the hub read/write files and execute commands on another
273
+ computer.** Read **[SECURITY.md](SECURITY.md)** before deploying — especially:
274
+
275
+ 1. **`nodeToken` is not verified by default** —— it is **mandatory** for public
276
+ deployments. Leaving it empty is only for fully trusted private networks
277
+ (Tailscale / same machine / trusted LAN).
278
+ 2. **Transport has no built-in TLS** —— public direct connections must sit
279
+ behind a TLS reverse proxy and use `wss://`.
280
+ 3. **`roots` is the real security boundary** —— grant only the directories the
281
+ tasks need, never a whole drive or the user home.
282
+
283
+ ---
284
+
285
+ ## Layout
286
+
287
+ | Path | Purpose |
288
+ |---|---|
289
+ | `lib/index.js` | Plugin entry (role dispatcher: `role: hub` / `worker` / `both`) |
290
+ | `lib/hub/**` | Hub side: node registry, 7 tools, subagent provider, UI routes |
291
+ | `lib/hub/views.js` | **Shared projection layer** —— tools and the UI use the same field whitelist |
292
+ | `lib/node/**` | Worker side: dial-in, task engine, artifact service, exec/agent backends |
293
+ | `lib/client.js` | Browser half (Web panel) |
294
+ | `lib/protocol.js` | Wire protocol (frame types, error codes, `PROTOCOL_VERSION`) |
295
+ | `lib/e2e.js` | Relay-link end-to-end encryption (X25519 handshake + ed25519 authentication + AES-256-GCM frame sealing) |
296
+ | `lib/setup.js` + `bin/dsh-remote-setup.js` | Beginner wizard (config generation / doctor check / plain-language connect-error translation) |
297
+ | `lib/relay.js` + `bin/dsh-remote-relay.js` | Relay (rendezvous point for NAT scenarios) |
298
+ | `profiles/**` | Deployment profiles and config snippets |
299
+ | `scripts/**` | Relay provisioning (systemd / scheduled task) + quality check scripts |
300
+
301
+ ---
302
+
303
+ ## Development
304
+
305
+ ```bash
306
+ npm install # only to verify the lockfile; zero runtime dependencies
307
+ npm run check # packaging self-consistency + doc drift + all tests
308
+ ```
309
+
310
+ **There is no build step.** The `.js` files in `lib/**` are exactly what DSH
311
+ loads (pure ESM, no transpilation).
312
+
313
+ Read **[CONTRIBUTING.md](CONTRIBUTING.md)** before changing code — it documents
314
+ several traps that **fail silently** (tool return values crossing the boundary,
315
+ cordis lifecycle, why `ctx.get()` is necessary).
316
+
317
+ > `node --test test/`(directory form)treats the directory as a module entry on
318
+ > Node 24 and always fails. Use `npm test`. PowerShell on Windows doesn't
319
+ > expand globs — don't pass `test/*.test.js` directly either.
320
+
321
+ ### Quality gates
322
+
323
+ | Layer | Command | Content |
324
+ |---|---|---|
325
+ | Packaging | `npm run check:packaging` | every path referenced by `files`/`exports`/`bin`/`dsh.bundle.patch` must exist |
326
+ | Doc drift | `npm run check:docs` | 102 assertions pinning key doc ⇄ code conventions |
327
+ | Behavior tests | `npm test` | 652 tests |
328
+
329
+ ---
330
+
331
+ ## Known issues
332
+
333
+ See **[KNOWN-ISSUES.md](KNOWN-ISSUES.md)** —— it records things **deliberately
334
+ not done**, items **pending verification**, and the debugging methodology.
335
+ Highlights:
336
+
337
+ - The worker `agent` backend doesn't work yet (`exec` does) —— under investigation
338
+ - The Windows 8.3 short-name bypass vector is unverified
339
+ - POSIX branches lack hands-on platform coverage (CI only)
340
+
341
+ ---
342
+
343
+ ## Changelog
344
+
345
+ See **[CHANGELOG.md](CHANGELOG.md)** —— includes a **root-cause analysis** for
346
+ every fix. During 0.x, `lib/**` is the loaded artifact, so any change may be a
347
+ behavior change — read it before upgrading.
348
+
349
+ ---
350
+
351
+ ## License
352
+
353
+ MIT —— see [LICENSE](LICENSE).
package/README.md CHANGED
@@ -1,3 +1,318 @@
1
- # Temporary Holding Version
1
+ # dsh-remote-agent
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/zbzbhs/dsh-remote-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/zbzbhs/dsh-remote-agent/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ ![Node](https://img.shields.io/badge/node-%E2%89%A522.19%20%7C%7C%20%3E%3D24-green)
6
+ ![dependencies](https://img.shields.io/badge/dependencies-zero-brightgreen)
7
+
8
+ **简体中文** | [English](README.en.md)
9
+
10
+ 把另一台电脑变成你 DSH 的**远程 Agent 同事**。
11
+
12
+ 副机拥有自己的 DSH 与独立 Agent 循环,任务在它本地持久运行;主电脑只拿到
13
+ **handoff 结果卡**与**产物清单**,成品要显式拉取才过河。
14
+
15
+ ```
16
+ ┌─────────────── 主电脑(hub)────────────────┐
17
+ │ DSH · 模型 · 会话日志 · 全部凭证 │
18
+ │ ┌─ dsh-remote-agent ─────────────────────┐ │
19
+ │ │ 7 个 remote_* 工具 + Web 面板 │ │
20
+ │ └────────────────────────────────────────┘ │
21
+ └──────────────────▲──────────────────────────┘
22
+ │ 节点主动拨入(direct / relay)
23
+ ┌──────────────────┴──────────────────────────┐
24
+ │ ┌─ 副电脑(worker)───────────────────────┐ │
25
+ │ │ 独立 Agent 循环 · 独立模型上下文 │ │
26
+ │ │ D:\work\** 中间产物留在这里 │ │
27
+ │ └────────────────────────────────────────┘ │
28
+ └─────────────────────────────────────────────┘
29
+ ```
30
+
31
+ | | |
32
+ |---|---|
33
+ | **状态** | 0.4.0 —— 主机侧端到端可用;真实运行时拨入已实测(独立进程 worker → 正在运行的 DSH 桌面 hub,见 [KNOWN-ISSUES §8](KNOWN-ISSUES.md));`exec` 与 `agent` 双链路均已在两台真机上验证通过 |
34
+ | **DSH** | 桌面版 / CLI,0.2.0-rc.2 验证 |
35
+ | **平台** | Windows(全量验证)· Linux / macOS(CI 覆盖)|
36
+ | **依赖** | **零外部 npm 依赖** —— 只用 `node:` 内置模块 |
37
+ | **Node** | `^22.19.0 \|\| >=24.0.0` |
38
+
39
+ **Web 面板**(侧栏「远程同事」):节点列表(含 CPU/内存徽章)、派发任务、
40
+ 任务状态、结果摘要、工作区变更、成品清单、日志尾巴。
41
+
42
+ <p align="center">
43
+ <img src="docs/screenshots/panel-main.png" width="720" alt="Web 面板:节点、派发表单、任务列表">
44
+ </p>
45
+
46
+ <p align="center">
47
+ <img src="docs/screenshots/panel-task-detail.png" width="720" alt="任务详情:结果摘要、工作区变更、成品清单">
48
+ </p>
49
+
50
+ ---
51
+
52
+ ## 五分钟上手
53
+
54
+ 按你的动手能力选一条路线,**每条都通向同一个终点**:
55
+
56
+ | 路线 | 主机 | 副机 | 适合 |
57
+ |---|---|---|---|
58
+ | **① 零终端**(Windows) | 双击 `bin/setup.cmd` → 回车 → Web 面板「副机接入」复制配对码 | 双击同一文件 → 粘配对码 → 回车自动写盘 | 见到黑窗口就慌 |
59
+ | **② 向导**(推荐) | 终端跑向导 → 回车确认写盘 | 同左,粘配对码 | 会开终端 |
60
+ | **③ 手动** | 参照下文配置节手写 | 手写(`inject: []` 必加) | 想完全掌控每行 |
61
+
62
+ 任一路线完成后:**两台重启 DSH**,对主机说一句「副机上有哪些节点在线?」——
63
+ 在线即通。出问题跑 `--doctor` 体检,每项 ✗ 都带怎么修。
64
+
65
+ ```bash
66
+ node bin/dsh-remote-setup.js
67
+ ```
68
+
69
+ > **Windows 不想开终端?** 在插件的 `bin/` 目录里**双击
70
+ > `setup.cmd`** —— 打开的窗口里全程回车即可。
71
+ > 插件装在 `%USERPROFILE%\.dsh\dsh_orb\dsh-plugin\dsh-remote-agent\bin\`。
72
+
73
+ 1. **主机上跑**:选「1=主机」→ 向导自动找本机局域网 IP、生成 `nt_` 强随机
74
+ `nodeToken`、输出**副机配对码**(一行 `dshra1://…`)与两台机器的完整 YAML
75
+ (含**防火墙放行命令**)。
76
+ 2. **副机上跑**:选「2=副机」→ **直接粘贴主机给的配对码**(或手抄地址与
77
+ token)→ 向导会**先探活主机端口**,然后生成副机配置。最坑的 `inject: []`
78
+ 已替你填好(不填它副机会永远静默不上线)。副机配置里也可以只写一行
79
+ `pair: '<配对码>'`,运行时自动展开成地址与 token(显式配置优先,写
80
+ `hubUrl:` 可临时覆盖)。
81
+ 3. 两台重启 DSH,对主机说一句「**副机上有哪些节点在线?**」—— 在线即通。
82
+ 4. **免终端路径(图形界面)**:打开主机的 Web 面板 →「**副机接入**」按钮 →
83
+ 一键生成并复制配对码(含防火墙放行命令)。主机面板派任务时也可在
84
+ 「模型」栏指定副机用的模型(留空用副机默认)。
85
+ 5. 出问题就体检(读取配置文件逐项检查,每项 ✗ 都带怎么修):
86
+
87
+ ```bash
88
+ node bin/dsh-remote-setup.js --doctor 你的配置.yml
89
+ ```
90
+
91
+ 不想交互?一条命令直接生成:`--role worker --hub-url 192.168.1.10:19387
92
+ --token nt_xxx --roots D:\work --node-id slave-pc`。
93
+
94
+ ---
95
+
96
+ ## 核心不变式
97
+
98
+ 这几条是这个包的设计前提,改动前请先读:
99
+
100
+ | 不变式 | 含义 |
101
+ |---|---|
102
+ | **凭证不离开副机** | 协议里不带模型 key;副机用自己的模型配置 |
103
+ | **断连不终止任务** | 任务在副机本地持久运行,中继挂了也跑完,重连后自动对账 |
104
+ | **中继只见密文** | relay 模式业务帧端到端加密(X25519 + AES-256-GCM,见 [SECURITY.md](SECURITY.md)):中继只看得到路由信封(`t`/`id`/`to`/`from`),goal 与成品内容全部密文 |
105
+ | **清单先于字节** | 任务结束只回 manifest + handoff;成品要 `remote_pull` 才传输 |
106
+ | **只有 `role:final` 过河** | 日志/缓存/中间构建文件只在副机留存,仅上报元数据 |
107
+ | **断点续传** | `remote_pull` 途中掉线/超时不从头重传:已收字节保留,重试自动从中段续传,全文 sha256 校验照旧 |
108
+
109
+ ---
110
+
111
+ ## 安装
112
+
113
+ ### 主机(hub)
114
+
115
+ ```bash
116
+ # 通过 DSH 插件安装器(GitHub 源)
117
+ dsh plugin add github:zbzbhs/dsh-remote-agent
118
+
119
+ # 或通过 npm
120
+ npm install -g dsh-remote-agent
121
+ ```
122
+
123
+ 或在 DSH 的插件对话框里填 `github:zbzbhs/dsh-remote-agent`。
124
+
125
+ 装完在 `cordis.patch.yml` 里确认这一行存在(包内 patch 会自动插入):
126
+
127
+ ```yaml
128
+ - id: remote-agent
129
+ name: dsh-remote-agent
130
+ inject: ['webServer', 'tools', 'systemPrompt']
131
+ config:
132
+ role: hub
133
+ ```
134
+
135
+ ### 副机(worker)
136
+
137
+ **同一份包**,但配置不同 —— 关键是**必须加 `inject: []`**:
138
+
139
+ ```yaml
140
+ - id: remote-agent
141
+ name: dsh-remote-agent
142
+ inject: [] # ← 副机必加,否则永不激活(见下方说明)
143
+ config:
144
+ role: worker
145
+ nodeId: slave-pc
146
+ transportMode: direct
147
+ hubUrl: 'ws://<主机地址>:19387/api/remote-agent/node'
148
+ token: '<与主机 nodeToken 相同,或留空>'
149
+ roots: ['D:\work'] # ← 改成副机真实存在的目录
150
+ ```
151
+
152
+ **嫌抄地址容易错?用配对码** —— 主机向导会输出一行 `dshra1://…`(内含
153
+ 地址与 token),副机配置里 `transportMode/hubUrl/token` 三行换成一行:
154
+
155
+ ```yaml
156
+ config:
157
+ role: worker
158
+ pair: 'dshra1://eyJ2IjoxLC…' # ← 主机向导输出的整行
159
+ roots: ['D:\work']
160
+ ```
161
+
162
+ > **配对码的两条规矩**
163
+ >
164
+ > 1. **它内含 token,等同凭证** —— 当面或加密聊天传,不要贴公开群。
165
+ > 2. **`roots` 不在配对码里** —— 副机能操作哪些目录是副机自己的安全
166
+ > 边界,永远由副机配置声明,主机侧的串决定不了它。
167
+ >
168
+ > 显式配置优先:`pair` 与 `hubUrl` 同时存在时 `hubUrl` 生效,方便临时换地址调试。
169
+ > 配对码坏了不会让副机起不来 —— 只记一条警告并按未配置处理(`--doctor` 会报)。
170
+
171
+ > **hub 身份认证(默认开启)**:副机也会验主机。hub 首次启动生成持久身份
172
+ > (指纹 `hubfp1-…`,落 `~/.dsh-remote-agent/hub-identity.json`),配对码
173
+ > 自动携带指纹 —— 副机粘码即完成首连钉住(TOFU,状态在副机
174
+ > `~/.dsh-remote-agent/hub-pin.json`)。手动配置可写
175
+ > `pinnedHubFingerprint: hubfp1-…` 显式钉住;之后指纹不匹配 = 拒绝上线并
176
+ > 停止重连(确认主动换 hub 后删 `hub-pin.json` 重启即可重新钉住)。
177
+ > 详见 [SECURITY.md](SECURITY.md) 检查清单第 7 条。
178
+
179
+ > **为什么副机要写 `inject: []`**
180
+ >
181
+ > 包内 `cordis.patch.yml` 给这一行声明了
182
+ > `inject: ['webServer','tools','systemPrompt']` —— 那是 **hub 需要的服务**。
183
+ > 副机 profile 里**没有**这些服务,不清空的话这一行会一直停在 **PENDING**
184
+ > **永远不激活**,副机也就永远不拨入。loader 按 id 覆盖,所以写 `inject: []`
185
+ > 就覆盖掉了包内的声明。
186
+
187
+ **接入路径**(双方都在 NAT 后 / 一方有公网 / 隧道)见
188
+ **[profiles/README.md](profiles/README.md)** —— 那里有每条路径的配置片段与验证命令,
189
+ 以及一条**降级链**:能直连走 direct,不通就降级到中继 VPS 或 cloudflared 隧道;
190
+ 链路全断时副机本地续跑、重连自动对账。中继的 VPS 部署模板(含 TLS 前置)
191
+ 在 [deploy/relay-vps/](deploy/relay-vps/)。relay 模式下业务帧走**端到端加密**
192
+ (中继只见密文,明文 goal/成品不流经中继;握手由副机主动发起,5 秒内无
193
+ ack 自动按明文兼容旧中继继续),安全边界与已知局限见
194
+ [SECURITY.md](SECURITY.md)。
195
+
196
+ ---
197
+
198
+ ## 用法
199
+
200
+ 装好后**不需要记命令**,直接说话:
201
+
202
+ | 你说 | agent 调用 |
203
+ |---|---|
204
+ | 「副机上有哪些节点在线?」 | `remote_nodes` |
205
+ | 「让副机在 D:\work 里跑一下测试」 | `remote_task` |
206
+ | 「那个任务怎么样了?」 | `remote_status` |
207
+ | 「给我个汇总」 | `remote_handoff` |
208
+ | 「有哪些成品?」 | `remote_manifest` |
209
+ | 「把 report.pdf 拉过来」 | `remote_pull` |
210
+ | 「取消那个任务」 | `remote_cancel` |
211
+
212
+ 另有一个 Web 面板(侧栏「远程同事」):节点列表(含 CPU/内存/负载与仓库清单)、
213
+ 派发任务、**待审批卡**(exec 任务默认挂起,批准才下发)、任务状态、结果摘要、
214
+ 工作区变更、**变更 diff**(文本文件改动的 unified diff)、成品清单、日志尾巴。
215
+
216
+ ### 工具速查
217
+
218
+ <details>
219
+ <summary><code>remote_task</code> 的参数</summary>
220
+
221
+ | 参数 | 说明 |
222
+ |---|---|
223
+ | `goal` | 任务目标。`mode=agent` 时是给副机 agent 的任务书;`mode=exec` 时是要执行的命令 |
224
+ | `node` | 目标节点 id。省略时自动选:多节点按在跑任务数/CPU/内存自动路由(agent 模式只选支持 agent 的节点) |
225
+ | `repo` | 按仓库路由:传仓库名 / 远端 url 片段 / 本机路径片段,任务派给持有该仓库的节点(多个时按负载自动选)。省略 `workspace` 时自动用仓库在本机的路径。副机接入后自动上报 roots 下的 git 仓库(凭证打码,每 10 分钟刷新) |
226
+ | `approval` | 两阶段审批:`manual`=挂起等面板批准后才派给副机(超时自动拒绝,fail-closed);`auto`=立即派发。省略时 **exec 默认 manual、agent 默认 auto** |
227
+ | `dryRun` | 只出预览不派发:返回会派到哪个节点/工作区/模式,路由与校验和正式派发同一条路径 |
228
+ | `workspace` | 副机上的绝对路径,必须在节点 `roots` 内。省略用默认 |
229
+ | `mode` | `agent`(副机起独立 agent 会话,默认)/ `exec`(直接跑命令)|
230
+ | `model` | 可选。副机用的模型(仅 `mode=agent` 生效):`{model:"deepseek-chat"}` 或 `{provider:"openai",model:"gpt-4o"}`。只传选择不传凭证 —— 副机用自己的 provider 配置跑;headless 后端不支持 |
231
+ | `outputs` | 期望产出物的 glob(相对工作区),用于产物清单筛选 |
232
+ | `timeoutMs` | 副机侧超时,默认 1 小时 |
233
+ | `denyPaths` | 额外禁止访问的绝对路径 |
234
+ | `artifactRules` | 覆盖内置产物角色判定,例如 `{ intermediate: ["build/**"] }` |
235
+
236
+ **`exec` 模式在 Windows 副机上跑的是 PowerShell** —— 用 `;` 分隔命令,
237
+ 不要用 `&`(会被拒)。需要 cmd 语法时显式 `cmd /c "..."`。
238
+
239
+ </details>
240
+
241
+ ---
242
+
243
+ ## 安全
244
+
245
+ **这个插件让主机能在另一台电脑上读写文件、执行命令。** 部署前请读
246
+ **[SECURITY.md](SECURITY.md)**,尤其这三条:
247
+
248
+ 1. **`nodeToken` 默认不校验** —— 公网部署**必须**配。留空只适用于完全可信的私网
249
+ (Tailscale / 同机 / 可信 LAN)。
250
+ 2. **传输不自带 TLS** —— 公网直连必须在前面放 TLS 反代,用 `wss://`。
251
+ 3. **`roots` 是真正的安全边界** —— 只给任务需要的目录,不要给整个盘或用户 home。
252
+
253
+ ---
254
+
255
+ ## 结构
256
+
257
+ | 路径 | 作用 |
258
+ |---|---|
259
+ | `lib/index.js` | 插件入口(角色分发器:`role: hub` / `worker` / `both`) |
260
+ | `lib/hub/**` | 主机侧:节点注册表、7 个工具、subagent provider、UI 路由 |
261
+ | `lib/hub/views.js` | **投影共享层** —— 工具与 UI 用同一份字段白名单 |
262
+ | `lib/node/**` | 副机侧:拨入、任务引擎、产物服务、exec/agent 双后端 |
263
+ | `lib/client.js` | 浏览器半边(Web 面板) |
264
+ | `lib/protocol.js` | 线协议(帧类型、错误码、`PROTOCOL_VERSION`) |
265
+ | `lib/e2e.js` | 中继链路端到端加密(X25519 握手 + ed25519 认证 + AES-256-GCM 帧封) |
266
+ | `lib/setup.js` + `bin/dsh-remote-setup.js` | 小白向导(配置生成 / doctor 体检 / 连接错误人话翻译) |
267
+ | `lib/relay.js` + `bin/dsh-remote-relay.js` | 中继(NAT 场景的会合点) |
268
+ | `profiles/**` | 部署档案与配置片段 |
269
+ | `scripts/**` | 中继初始化(systemd / 计划任务)+ 质量检查脚本 |
270
+
271
+ ---
272
+
273
+ ## 开发
274
+
275
+ ```bash
276
+ npm install # 只为验证 lockfile;本包零外部运行时依赖
277
+ npm run check # 打包自洽性 + 文档漂移 + 全部测试
278
+ ```
279
+
280
+ **没有构建步骤。** `lib/**` 里的 `.js` 就是 DSH 实际加载的产物(纯 ESM,无转译)。
281
+
282
+ 改代码前请读 **[CONTRIBUTING.md](CONTRIBUTING.md)** —— 那里记了几个
283
+ **会静默失败**的陷阱(工具返回值出边界、cordis 生命周期、`ctx.get()` 的必要性)。
284
+
285
+ > `node --test test/`(目录形式)在 Node 24 下会把目录当模块入口,必然失败。
286
+ > 用 `npm test`。Windows 的 PowerShell 不展开 glob,也别直接传 `test/*.test.js`。
287
+
288
+ ### 质量防线
289
+
290
+ | 层 | 命令 | 内容 |
291
+ |---|---|---|
292
+ | 打包自洽 | `npm run check:packaging` | `files`/`exports`/`bin`/`dsh.bundle.patch` 指向的路径必须存在 |
293
+ | 文档漂移 | `npm run check:docs` | 102 条断言,锁住文档与代码的关键约定 |
294
+ | 行为测试 | `npm test` | 652 条 |
295
+
296
+ ---
297
+
298
+ ## 已知问题
299
+
300
+ 见 **[KNOWN-ISSUES.md](KNOWN-ISSUES.md)** —— 记录了**刻意不做**的项、
301
+ **待补验**的项、以及排查方法论。要点:
302
+
303
+ - 副机 `agent` 后端不工作(`exec` 正常)—— 待定位
304
+ - Windows 8.3 短名绕过向量未验证
305
+ - POSIX 分支的平台实测覆盖不足(只跑过 CI)
306
+
307
+ ---
308
+
309
+ ## 更新日志
310
+
311
+ 见 **[CHANGELOG.md](CHANGELOG.md)** —— 含每个修复的**根因分析**。
312
+ 0.x 期间 `lib/**` 是加载产物,任何变更都可能是行为变更,升级前请读。
313
+
314
+ ---
315
+
316
+ ## 许可
317
+
318
+ MIT —— 见 [LICENSE](LICENSE)。