@hope_phenom/dsh-plugin-remote-use 0.0.0-stage → 0.2.24

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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -2
  3. package/README.zh.md +195 -0
  4. package/assets/icon.svg +47 -0
  5. package/cordis.patch.yml +41 -0
  6. package/lib/bridge/acp-bridge.js +905 -0
  7. package/lib/bridge/peer-connection.js +730 -0
  8. package/lib/client.js +2842 -0
  9. package/lib/core/acp-model-overlay.js +121 -0
  10. package/lib/core/approval-queue.js +216 -0
  11. package/lib/core/audit-log.js +322 -0
  12. package/lib/core/desktop-notify.js +173 -0
  13. package/lib/core/environment-prompt.js +76 -0
  14. package/lib/core/relay-supervisor.js +278 -0
  15. package/lib/core/review-engine.js +679 -0
  16. package/lib/core/sandbox-diagnosis.js +258 -0
  17. package/lib/core/sandbox-probe.js +310 -0
  18. package/lib/core/self-acp-spec.js +117 -0
  19. package/lib/core/selfcheck-runner.js +150 -0
  20. package/lib/core/settings-store.js +226 -0
  21. package/lib/core/shot-gc.js +334 -0
  22. package/lib/core/task-cwd.js +50 -0
  23. package/lib/core/task-memory.js +163 -0
  24. package/lib/core/task-registry.js +238 -0
  25. package/lib/core/toolcall-cache.js +206 -0
  26. package/lib/core/transfer-queue.js +58 -0
  27. package/lib/core/transfer.js +1144 -0
  28. package/lib/core/worker-arming.js +334 -0
  29. package/lib/index.js +2207 -0
  30. package/lib/provider/out-of-process-helpers.js +76 -0
  31. package/lib/provider/remote-provider.js +261 -0
  32. package/lib/tools/mediator-shim.mjs +180 -0
  33. package/lib/tools/prepare-sandbox-mediator.mjs +529 -0
  34. package/lib/tools/remote-tools.js +680 -0
  35. package/lib/tools/selfcheck-fix-prompt.md +206 -0
  36. package/locale/en.json +6 -0
  37. package/locale/zh.json +6 -0
  38. package/package.json +72 -4
  39. package/slave/acp-profile-approval.patch.yml +66 -0
  40. package/slave/file-transfer.mjs +552 -0
  41. package/slave/integrity-probe.ps1 +99 -0
  42. package/slave/make-launch-spec.mjs +184 -0
  43. package/slave/net-address.mjs +138 -0
  44. package/slave/node_modules/.package-lock.json +26 -0
  45. package/slave/node_modules/ws/LICENSE +20 -0
  46. package/slave/node_modules/ws/README.md +548 -0
  47. package/slave/node_modules/ws/browser.js +8 -0
  48. package/slave/node_modules/ws/index.js +22 -0
  49. package/slave/node_modules/ws/lib/buffer-util.js +131 -0
  50. package/slave/node_modules/ws/lib/constants.js +19 -0
  51. package/slave/node_modules/ws/lib/event-target.js +292 -0
  52. package/slave/node_modules/ws/lib/extension.js +203 -0
  53. package/slave/node_modules/ws/lib/limiter.js +55 -0
  54. package/slave/node_modules/ws/lib/permessage-deflate.js +530 -0
  55. package/slave/node_modules/ws/lib/receiver.js +743 -0
  56. package/slave/node_modules/ws/lib/sender.js +607 -0
  57. package/slave/node_modules/ws/lib/stream.js +161 -0
  58. package/slave/node_modules/ws/lib/subprotocol.js +62 -0
  59. package/slave/node_modules/ws/lib/validation.js +152 -0
  60. package/slave/node_modules/ws/lib/websocket-server.js +562 -0
  61. package/slave/node_modules/ws/lib/websocket.js +1421 -0
  62. package/slave/node_modules/ws/package.json +74 -0
  63. package/slave/node_modules/ws/wrapper.mjs +21 -0
  64. package/slave/pairing.mjs +175 -0
  65. package/slave/permission-pairing.mjs +240 -0
  66. package/slave/prune-shots.mjs +170 -0
  67. package/slave/relay.mjs +1351 -0
  68. package/slave/repair-workspace-integrity.ps1 +75 -0
  69. package/slave/screenshot.ps1 +114 -0
  70. package/slave/selfcheck.ps1 +350 -0
  71. package/slave/stage-app.ps1 +116 -0
  72. package/slave/workspace-integrity.mjs +157 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hope.Phenom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,208 @@
1
- # Temporary Holding Version
1
+ # @hope_phenom/dsh-plugin-remote-use
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
+ [中文说明 / Chinese: `README.zh.md`](README.zh.md)
4
+
5
+ Delegate [DSH](https://www.deepseek.com/) tasks to a paired **worker** on your LAN and drive it like a subagent.
6
+
7
+ **What it solves.** GUI automation and any verification that only a screen can answer has to run on a
8
+ machine that can really move a mouse and a keyboard. Doing that locally means **fighting the operator
9
+ for their own computer** — stealing focus, the cursor, the clipboard. This plugin pushes that part of
10
+ the work onto a dedicated node and keeps the reasoning on the master.
11
+
12
+ ```text
13
+ master DSH ── ACP over WebSocket ──▶ worker relay ──▶ worker DSH (runs the task, screenshots, sees the GUI)
14
+ │ ▲
15
+ └── approvals / audit / per-session switch ──┘
16
+ ```
17
+
18
+ ## Install
19
+
20
+ **Install it through DSH's plugin manager** — it does all three things at once: fetches the package,
21
+ registers it in the profile's `bundles`, and enables it.
22
+
23
+ ```
24
+ Plugin manager → Install → @hope_phenom/dsh-plugin-remote-use
25
+ ```
26
+
27
+ Then **restart DSH**. That is the whole install: it is the same path any plugin takes.
28
+
29
+ <details>
30
+ <summary>Manual install (the fallback if the plugin manager will not cooperate)</summary>
31
+
32
+ ```powershell
33
+ # Install into a profile -- this only puts the files there.
34
+ npm install --prefix "$env:USERPROFILE\.dsh\profiles\<profile>" @hope_phenom/dsh-plugin-remote-use
35
+ ```
36
+
37
+ **Installing is not enough — the package name must also be added to that profile's
38
+ `dsh.profile.bundles`,** or it will not be loaded. The profile's `package.json` looks like this:
39
+
40
+ ```json
41
+ "dsh": { "profile": { "bundles": [ "@deepseek-ai/dsh-base", "@hope_phenom/dsh-plugin-remote-use" ] } }
42
+ ```
43
+
44
+ Then add **one kind** of entry to that profile's patch layer: the plugin's own config — the worker
45
+ list (url / token / cwd).
46
+
47
+ **No per-worker tool row is needed** (0.2.0 and later). Earlier versions asked for a
48
+ `dsh-tool-subagent` row per worker with `provider: remote-<worker id>`; that route does not work
49
+ even when written correctly, because a Web session's model-facing tool catalog is enumerated by the
50
+ **agent preset**, not by the loader tree. The plugin now registers its own delegation tool,
51
+ `remote_subagent`, by the same route its four `remote_*` tools already use. If you wrote a
52
+ `tool-subagent-remote-*` block from the old instructions, you can delete it.
53
+
54
+ </details>
55
+
56
+ > **Full instructions — copy-pasteable YAML, worker-side deployment, the security model and
57
+ > troubleshooting — are in the
58
+ > [repository README](https://github.com/Hope-Phenom/dsh-plugin-remote-use#readme).**
59
+ > This file covers only what an npm page has to make clear.
60
+
61
+ ## Use (the short version)
62
+
63
+ 1. **It is off by default.** In the session that should be able to reach a worker, type:
64
+
65
+ ```text
66
+ /remote-use on
67
+ ```
68
+
69
+ The switch is **per session and does not persist** — a restart puts it back to off, and in a new
70
+ session the remote tools are not visible.
71
+
72
+ 2. Let the model do the work that needs a GUI. It will call `remote_selfcheck` first to confirm the
73
+ node is ready, and only then delegate.
74
+
75
+ | Tool the model sees | What it is for |
76
+ |---|---|
77
+ | `remote_subagent` | **Delegate a task to a paired worker**, and drive it like a subagent. `run_in_background: true` keeps the current conversation free |
78
+ | `remote_selfcheck` | The node's readiness report. **Call it before delegating** — a node that is not ready is refused by the dispatch gate |
79
+ | `remote_agents` | Task progress; `action: status` / `wait` for one task, and **it returns that task's answer text once the task has settled** |
80
+ | `remote_screenshot` | Capture the node's screen to the master, then look at it with `read_image` |
81
+ | `remote_file` | **Move files to and from a node** — `action: send` / `fetch` / `list` / `stat`. No SSH, no `scp`, no setup on the node, and it does **not** occupy the node's single GUI task slot, so a copy can run while a visual task is in flight |
82
+ | `remote_interrupt` | Stop a task (cooperative) |
83
+
84
+ Collecting a background delegation's answer — the only route back:
85
+
86
+ ```text
87
+ remote_subagent { prompt: "…", run_in_background: true } -> taskId: rtask_xxx
88
+ remote_agents { action: "status", taskId: "rtask_xxx" } -> state + the full answer
89
+ ```
90
+
91
+ ⚠️ `run_in_background: true` is **not "return immediately"**: a node runs one task at a time (the GUI is
92
+ an exclusive resource), so when the node is busy the call **first waits for its turn** (measured: 32
93
+ seconds on a busy worker).
94
+
95
+ The settings page (**Settings → Remote work nodes**) manages nodes, handles **escalation approvals**,
96
+ shows the **audit trail**, edits the delegation policy, and can **arm this machine as a worker**
97
+ (symmetric pairing).
98
+
99
+ > **The task directory (`cwd`) is an absolute path on the worker**: that is where the task runs and
100
+ > **the scope its sandbox may write to**. Fill it in once on the worker's arming panel and it goes
101
+ > **into the pairing string** (**0.2.1 and later**), so the master picks it up from the paste.
102
+ > The master **refuses to save** a worker without a usable task directory — such a worker can never
103
+ > accept a task.
104
+
105
+ ## Security model (the essentials)
106
+
107
+ - **Off by default** — a new session injects no policy and cannot see the remote tools at all
108
+ - **The sandbox must actually hold** — if confinement cannot be *proved*, worker mode is **refused**,
109
+ not warned about. What the worker needs (a materialized mediator, plus a `sandbox` row naming it) is
110
+ **hand-deployed and can disappear**; the arming panel therefore reports it on **page load** and hands
111
+ back a ready-to-run command (`lib/tools/prepare-sandbox-mediator.mjs`) instead of leaving the operator
112
+ to reverse-engineer a three-layer error.
113
+ - **Escalation is never auto-approved** — `needsHuman` / `danger-full-access` go to a human queue, and
114
+ a timeout counts as a refusal
115
+ - **The session's own sandbox level is a deployment setting, and it defaults to `danger-full-access`** —
116
+ set it on the machine acting as the worker (**Settings → Remote work nodes → the sandbox level for this
117
+ machine's remote sessions**), and it is written into the launch spec when the worker role is armed. It is
118
+ a deployment value, not a per-task negotiation, for a measured reason: a GUI process that cannot start
119
+ under a restricted token fails **silently** (the command returns success; only a desktop dialog is left),
120
+ so that failure can never reach the approval queue. Set `workspace-write` for a worker that only runs
121
+ confined work. The master sees the level the node actually reports.
122
+ - **Credentials never reach the browser** — the page only ever gets a mask
123
+ - The node must be **genuinely logged in to a desktop**: input injection does not work on a locked screen
124
+
125
+ ## Keeping the launch spec in sync
126
+
127
+ `workerSessionMode` is a **deployment value**: it is written into the ACP launch spec when the worker
128
+ role is armed, not applied to a session already running.
129
+
130
+ ### The model used when arming (`workerModel`)
131
+
132
+ **Settings → Remote workers → Model for this machine's remote sessions**, filled in as
133
+ `provider/model` (for example `deepseek-official/deepseek-flash`). It is a deployment value of the
134
+ same kind as the mode, but it decides which model the worker's agent runs:
135
+
136
+ - it is applied by a plugin-owned **`--patch` overlay** at launch, so it does **not** edit your
137
+ `cordis.patch.yml`;
138
+ - empty means "use whatever the worker profile configures" (the previous behaviour);
139
+ - it decides `initialize`'s `promptCapabilities.image`: a model that does not declare image input
140
+ makes the master **unable to send pictures** and breaks `read_image` on the worker, and that is
141
+ **silent**. Arming therefore resolves the id against this machine's catalogue first: an unknown
142
+ id **refuses to arm**, and a model without image support produces a warning (a text-only worker
143
+ is a legitimate choice);
144
+ - changing it needs **re-arming**: the overlay is part of the launch spec, and a running relay does
145
+ not replace it.
146
+
147
+ > Why not per task: `dsh-acp` fixes the image capability at `initialize` from the configured
148
+ > provider/model, and it is fixed for that connection. `session/set_config_option` changes the
149
+ > route but does not re-advertise the capability, so this can only be a deployment value.
150
+
151
+ - **Supervised worker** (the plugin owns the relay): saving a new mode while a listener is running
152
+ does **not** rewrite that listener's spec. The settings page now shows **both** the saved value and
153
+ the mode the running relay was started with, and says **re-arm required** when they differ. Stop
154
+ and arm again to apply it; the pairing token survives, so no re-pairing is needed.
155
+ - **Hand-managed relay** (a scheduled task running `relay.mjs --cmd-file agent-spec.json`):
156
+ `node slave/make-launch-spec.mjs --out agent-spec.json` **without** `--permission-mode` now reads
157
+ `workerSessionMode` from the plugin's settings file, so the manual path follows the page. An
158
+ explicit `--permission-mode` still wins. The relay reads the same settings file and reports
159
+ `sessionModeMismatch` plus a warning check (`S19`) when its spec disagrees with the setting —
160
+ including a loud line at startup. Regenerate the spec and restart the relay.
161
+
162
+ > **If you do not want to redeploy `slave/` for every plugin upgrade**, stop the scheduled task and
163
+ > arm this machine as a worker from **Settings → Remote workers → This machine's role**. The relay
164
+ > then runs from the installed package, so a plugin upgrade plus a DSH restart is enough; the launch
165
+ > spec is a temp file regenerated on each arming. The trade-off is that the relay runs only while
166
+ > DSH does — keep the scheduled task if you need a listener before login.
167
+
168
+ ## GUI apps in a workspace: the Low-integrity trap
169
+
170
+ A worker that has ever run `workspace-write` carries an **inheritable Low mandatory label** on its
171
+ workspace. Files copied into that tree inherit the label, and Windows launches a Low-labelled image
172
+ at **Low integrity even when the parent token is High**. The symptom is a GUI app that cannot write
173
+ `%LOCALAPPDATA%` (`UnauthorizedAccessException`) while the agent shell writes the same path fine — it
174
+ looks like an ACL or application bug, not a sandbox leftover.
175
+
176
+ - Under `danger-full-access`, the relay **repairs an explicitly armed task directory** on start
177
+ (`icacls ... /setintegritylevel (OI)(CI)Medium /T`). This is the correct counterpart to removing
178
+ the sandbox: the label is no longer protecting anything. Pass `--no-integrity-repair` to keep it,
179
+ or `--repair-integrity` to allow the relay's inherited cwd (still never a system directory).
180
+ - A hand-managed relay should pass `--task-dir <workspace>` so both the repair and the S20 probe
181
+ have a directory to work on. To repair by hand:
182
+ `powershell -File slave/repair-workspace-integrity.ps1 -Path <workspace>`.
183
+ - To keep GUI payloads out of the workspace entirely:
184
+ `powershell -File slave/stage-app.ps1 -Source <app-dir> [-Launch]`. It copies to
185
+ `%LOCALAPPDATA%\dsh-remote-use\staging\...` and sets the staged tree to Medium.
186
+ - `remote_selfcheck` reports the workspace and `%TEMP%` readings as `integrity` and as check `S20`.
187
+ A degraded reading is a **warning**, because the machine is still reachable — but GUI work will
188
+ fail until the label is fixed.
189
+
190
+ ## Requirements
191
+
192
+ - Node ≥ 20
193
+ - DSH installed on the master and on the worker
194
+ - The worker needs an **unlocked interactive desktop session**
195
+ - The worker runs `slave/relay.mjs` (shipped in this package, together with the `ws` dependency it needs)
196
+
197
+ ## Documentation
198
+
199
+ | | |
200
+ |---|---|
201
+ | [Repository README](https://github.com/Hope-Phenom/dsh-plugin-remote-use#readme) | Full install, use and operations |
202
+ | [Design notes](https://github.com/Hope-Phenom/dsh-plugin-remote-use/blob/master/docs/DESIGN.md) | Trade-offs and the decision record |
203
+ | [Pitfalls](https://github.com/Hope-Phenom/dsh-plugin-remote-use/blob/master/docs/PITFALLS.md) | Problems actually hit during development, and **how each one was found** |
204
+ | [Interface contract](https://github.com/Hope-Phenom/dsh-plugin-remote-use/blob/master/docs/INTERFACES.md) | Upstream APIs and integration facts |
205
+
206
+ ## Licence
207
+
208
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,195 @@
1
+ # @hope_phenom/dsh-plugin-remote-use
2
+
3
+ [English: `README.md`](README.md)
4
+
5
+ 把 [DSH](https://www.deepseek.com/) 的任务委托给局域网里另一台已配对的**工作节点**执行,像调用子 Agent 一样驱动它。
6
+
7
+ **解决什么问题**:GUI 自动化测试和"看屏幕才知道结果"的验证必须在一台真能操作鼠标键盘的机器上跑。
8
+ 在本机做就会**和操作者抢电脑**——抢焦点、抢光标、抢剪贴板。这个插件把那部分工作推到一台专门的节点上,
9
+ 任务的核心逻辑仍留在主机。
10
+
11
+ ```text
12
+ 主机 DSH ── ACP over WebSocket ──▶ 从机中继 ──▶ 从机 DSH(在从机本地跑任务、截图、看 GUI)
13
+ │ ▲
14
+ └── 审批 / 审计 / 会话级开关 ────────┘
15
+ ```
16
+
17
+ ## 安装
18
+
19
+ **通过 DSH 的插件管理器安装**(它能同时完成装包、注册到 profile 的 `bundles`、启用三件事):
20
+
21
+ ```
22
+ 插件管理器 → 安装 → @hope_phenom/dsh-plugin-remote-use
23
+ ```
24
+
25
+ 装完**重启 DSH**。这就是全部 —— 插件管理器是普通用户装任何插件的那条路。
26
+
27
+ <details>
28
+ <summary>手动安装(插件管理器不好用时的退路)</summary>
29
+
30
+ ```powershell
31
+ # 装到某个 profile(这一步只把文件放进去)
32
+ npm install --prefix "$env:USERPROFILE\.dsh\profiles\<profile>" @hope_phenom/dsh-plugin-remote-use
33
+ ```
34
+
35
+ **光装不够 —— 还必须把包名加进该 profile 的 `dsh.profile.bundles`**,否则它不会被加载。
36
+ profile 的 `package.json` 长这样:
37
+
38
+ ```json
39
+ "dsh": { "profile": { "bundles": [ "@deepseek-ai/dsh-base", "@hope_phenom/dsh-plugin-remote-use" ] } }
40
+ ```
41
+
42
+ 然后在该 profile 的补丁层里加**一类**条目:插件自身的配置 —— worker 列表(url / token / cwd)。
43
+
44
+ **不需要**为每台从机写"工具行"(0.2.0 起)。早期版本要求加一条 `dsh-tool-subagent` 行、
45
+ `provider: remote-<worker id>`;那条路**即使写对也不生效**,因为 Web 会话的模型工具目录由
46
+ **agent preset** 枚举,而不是由 loader 树决定。插件现在自己注册委派工具 `remote_subagent`,
47
+ 走的是它四个 `remote_*` 工具已经在用的那条路。若你按旧文档写过
48
+ `tool-subagent-remote-*` 那一块,可以直接删掉。
49
+
50
+ </details>
51
+
52
+ > **完整的安装步骤、可直接抄的 YAML、从机侧部署、安全模型与排查方法,见
53
+ > [仓库 README](https://github.com/Hope-Phenom/dsh-plugin-remote-use#readme)。**
54
+ > 这份文件只覆盖 npm 页面上必须说清楚的部分。
55
+
56
+ ## 使用(速览)
57
+
58
+ 1. **默认是关闭的。** 在需要用到工作节点的那个会话里输入:
59
+
60
+ ```text
61
+ /remote-use on
62
+ ```
63
+
64
+ 开关**按会话生效且不持久化**——重启后回到 off,新会话里远程工具不可见。
65
+
66
+ 2. 让模型去做需要 GUI 的事。它会先调 `remote_selfcheck` 确认节点可用,再委托。
67
+
68
+ | 模型可见的工具 | 用途 |
69
+ |---|---|
70
+ | `remote_subagent` | **把任务派到工作节点上执行**,像调用子 Agent 一样。`run_in_background: true` 则不占住当前对话 |
71
+ | `remote_selfcheck` | 节点就绪报告。**委托前先调它**——未就绪的节点会被派单门禁拒绝 |
72
+ | `remote_agents` | 查看任务进度;`action: status` / `wait` 盯某一个任务,**任务落定后把它的答案正文一并取回** |
73
+ | `remote_screenshot` | 抓节点屏幕并落盘到主机,然后用 `read_image` 看 |
74
+ | `remote_file` | **在主节点与工作节点之间传文件**——`action: send` / `fetch` / `list` / `stat`。不需要 SSH、不需要 `scp`、节点上不需要任何配置,而且它**不占用节点唯一的 GUI 任务槽**,所以 GUI 任务在跑的时候也能传 |
75
+ | `remote_interrupt` | 停止任务(协作式) |
76
+
77
+ 后台派单的结果这样取回(这是后台模式唯一的路):
78
+
79
+ ```text
80
+ remote_subagent { prompt: "…", run_in_background: true } → taskId: rtask_xxx
81
+ remote_agents { action: "status", taskId: "rtask_xxx" } → 状态 + 完整答案
82
+ ```
83
+
84
+ ⚠️ `run_in_background: true` **不是"立刻返回"**:一台节点同时只跑一个任务(GUI 是独占资源),
85
+ 节点忙时这个调用会**先排队等轮到自己**(实测等过 32 秒)。
86
+
87
+ 设置页(**设置 → 远程工作节点**)可以管理节点、处理**提权审批**、查看**审计留痕**、
88
+ 调整委派策略,以及把本机**布防为工作节点**(对称配对)。
89
+
90
+ > **任务目录(`cwd`)是从机上的绝对路径**,任务在那里运行,**也是沙箱允许写入的范围**。
91
+ > 在从机的布防面板里填一次,它会**写进配对串**(**0.2.1 起**),主机粘贴后自动带上。
92
+ > 主机侧**拒绝保存**没有可用任务目录的节点——那种配置永远无法派单成功。
93
+
94
+ ## 安全模型(要点)
95
+
96
+ - **默认关闭** —— 新会话不注入策略、远程工具在该会话内不可见
97
+ - **沙箱必须真实生效** —— 沙箱无法被证明时**拒绝**启用 worker 模式,不是警告。
98
+ 从机需要的两样东西(mediator 目录、指名它的 `sandbox` 行)都是**手工部署物**,会消失;
99
+ 所以「本机角色」在**页面加载时**就报告状态,并在缺失时给出**填好本机路径、可直接执行**的命令
100
+ (`lib/tools/prepare-sandbox-mediator.mjs`)。它只物化 + 仅在 profile 没有该行时追加 + 跑同一个
101
+ 探针,幂等、可 `--dry-run`;**页面不会替你执行** —— 写的是插件自身状态之外的文件。
102
+ - **提权永不自动批准** —— `needsHuman` / `danger-full-access` 进人工审批队列,超时按拒绝
103
+ - **会话自己的沙箱档位是部署设置,默认 `danger-full-access`** —— 在作为工作节点的那台机器上设置
104
+ (**设置 → 远程工作节点 → 本机远程会话的沙箱档位**),布防时写进启动规格。它是**部署值**而不是逐任务
105
+ 谈判,理由是实测的:GUI 程序在受限令牌下起不来是**静默失败**(命令返回成功,只留一个桌面对话框),
106
+ 因此这类失败**永远进不了审批队列**。只跑受限任务的工作节点就设成 `workspace-write`。
107
+ 主机侧能看到该节点**实际自报**的档位。
108
+ - **凭据不回传浏览器** —— 页面只拿到掩码
109
+ - 节点侧必须**真的登录进桌面**:锁屏时无法注入输入
110
+
111
+ ## 让启动规格与设置保持一致
112
+
113
+ `workerSessionMode` 是**部署值**:布防时写进 ACP 启动规格,**不会**改动已经在运行的会话。
114
+
115
+ ### 布防时的模型(`workerModel`)
116
+
117
+ **设置 → 远程工作节点 → 本机远程会话的模型**,填 `provider/model`(例如 `deepseek-official/deepseek-flash`)。
118
+ 它和档位是同一类**部署值**,但作用的是"从机的 agent 用哪个模型":
119
+
120
+ - 布防时以插件自己的 **`--patch` overlay** 注入 ACP 启动配置,**不修改你的 `cordis.patch.yml`**;
121
+ - 留空 = 沿用从机 profile 里配的模型(旧行为);
122
+ - 它决定 `initialize` 的 `promptCapabilities.image`:模型不声明 image 输入时,主机**不能**向从机发图,
123
+ 从机的 `read_image` 也会失败 —— 而且这是**静默**的。所以布防时会先用本机模型目录解析这个 id:
124
+ **不存在就拒绝布防**,存在但不带图则给警告(纯文本节点是合法选择,只是别指望它读图);
125
+ - 改了需要**重新布防**:overlay 是启动规格的一部分,运行中的中继不会自己换。
126
+
127
+ > 为什么不能按任务指定:`dsh-acp` 在 `initialize` 时就按**配置的** provider/model 定下图片能力,并固定给这条连接;
128
+ > `session/set_config_option` 只改会话路由,不会重新 advertise。所以它是部署值,不是逐任务参数。
129
+
130
+ ### 从机环境自动配置(提示词)
131
+
132
+ 从机的 GUI 就绪项(虚拟显示器、分辨率与原点、QuickEdit、屏保、睡眠/休眠、被测应用……)可以**交给从机自己的
133
+ DSH 会话**去做:把随包发布的 [`lib/tools/selfcheck-fix-prompt.md`](lib/tools/selfcheck-fix-prompt.md) 整段粘进
134
+ 从机的对话即可。它会:
135
+
136
+ - 找到并运行 `slave/selfcheck.ps1`,读 `selfcheck.json`;
137
+ - 按报告逐项修复(blocking 优先),每步贴原始输出;
138
+ - 需要管理员、装驱动、或**需要人登录/解锁**的步骤,停下来说明,不假装成功;
139
+ - 最后复跑自检并对比修复前后的 `checks`。
140
+
141
+ > 自检必须在**已登录、已解锁的交互式桌面会话**里跑(DSH 桌面进程所在的那个)。SSH/服务会话里跑出来的
142
+ > `session=0`、S1=0 块屏是假阴性。
143
+
144
+ - **插件托管的从机**(中继由插件拉起):运行中修改档位**不会**改写那个中继的启动规格。设置页现在
145
+ 同时显示**保存值**和**运行中中继实际使用的档位**,两者不一致时明确提示**需要重新布防**。先停止
146
+ 再布防即可生效;凭据跨重启保留,所以**不需要重新配对**。
147
+ - **手管中继**(计划任务直接跑 `relay.mjs --cmd-file agent-spec.json`):不带 `--permission-mode`
148
+ 运行 `node slave/make-launch-spec.mjs --out agent-spec.json` 时,它会读取插件设置文件里的
149
+ `workerSessionMode`,所以手工路径也跟随设置页;显式传 `--permission-mode` 仍然优先。中继自己也会
150
+ 读同一个设置文件,在规格与设置不一致时报告 `sessionModeMismatch` 和一条警告检查(`S19`),启动时
151
+ 还会打一行醒目日志。重建规格并重启中继即可。
152
+
153
+ > **如果不想每次升级插件都重新部署 `slave/`**,停掉计划任务,改用设置页的
154
+ > **远程工作节点 → 本机角色 → 布防为工作节点**。中继会从已安装的插件包启动,升级插件 + 重启 DSH
155
+ > 就够;启动规格是临时文件,每次布防自动重建。代价是中继只在 DSH 运行时存在 —— 如果要在登录前
156
+ > 就监听,请保留计划任务。
157
+
158
+ ## 工作目录里的 GUI:低完整性陷阱
159
+
160
+ 只要工作目录曾经跑过 `workspace-write`,它就会带上**可继承的 Low 强制标签**。复制进去的文件继承
161
+ 该标签,而 Windows 会把带 Low 标签的镜像**以低完整性启动**——即使父进程令牌是 High。现象是 GUI 程序
162
+ 写 `%LOCALAPPDATA%` 抛 `UnauthorizedAccessException`,而 agent shell 写同一路径完全正常,看起来像
163
+ ACL 或应用缺陷,而不是沙箱残留。
164
+
165
+ - `danger-full-access` 下,中继会在启动时**修复显式布防的任务目录**(`icacls ...
166
+ /setintegritylevel (OI)(CI)Medium /T`)。这是关掉沙箱后的正确配套:那个标签已经不再保护任何东西。
167
+ 传 `--no-integrity-repair` 可保留标签;传 `--repair-integrity` 才允许修复中继继承来的 cwd(系统目录
168
+ 仍然拒绝)。
169
+ - 手管中继应传 `--task-dir <workspace>`,这样自动修复与 S20 探针都有明确的目录。手工修复:
170
+ `powershell -File slave/repair-workspace-integrity.ps1 -Path <workspace>`。
171
+ - 如果希望 GUI 载荷完全待在工作目录之外:
172
+ `powershell -File slave/stage-app.ps1 -Source <app-dir> [-Launch]`,它会复制到
173
+ `%LOCALAPPDATA%\dsh-remote-use\staging\...` 并把暂存目录设为 Medium。
174
+ - `remote_selfcheck` 会把工作目录与 `%TEMP%` 的读数写进 `integrity`,并作为 `S20` 检查上报。降级是
175
+ **警告**而不是阻断:机器仍然可达,但 GUI 工作会一直失败,直到标签被修复。
176
+
177
+ ## 运行要求
178
+
179
+ - Node ≥ 20
180
+ - 主机与工作节点都装 DSH
181
+ - 工作节点有一个**已解锁的交互式桌面会话**
182
+ - 工作节点上运行 `slave/relay.mjs`(随包提供,含它需要的 `ws` 依赖)
183
+
184
+ ## 文档
185
+
186
+ | | |
187
+ |---|---|
188
+ | [仓库 README](https://github.com/Hope-Phenom/dsh-plugin-remote-use#readme) | 完整安装、使用、运维 |
189
+ | [设计文档](https://github.com/Hope-Phenom/dsh-plugin-remote-use/blob/master/docs/DESIGN.md) | 设计取舍与决策记录 |
190
+ | [踩坑与教训](https://github.com/Hope-Phenom/dsh-plugin-remote-use/blob/master/docs/PITFALLS.md) | 开发中真实遇到的问题及**发现方式** |
191
+ | [接口契约](https://github.com/Hope-Phenom/dsh-plugin-remote-use/blob/master/docs/INTERFACES.md) | 上游 API 与集成事实 |
192
+
193
+ ## 许可
194
+
195
+ MIT
@@ -0,0 +1,47 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Remote workers">
2
+ <!--
3
+ Two machines, offset in both axes and overlapping front to back: the network
4
+ neighbourhood convention, which reads as "another computer" rather than "my second
5
+ monitor".
6
+
7
+ OCCLUSION WITHOUT A FILL. The obvious way to stack these is to fill the front machine
8
+ with the background colour so it hides the back one, but this icon set is stroke only
9
+ (fill="none") and the Settings page has both dark and light themes, so a hardcoded
10
+ background would show as a coloured block on the other one. Instead the back machine's
11
+ outline is DRAWN ONLY WHERE IT IS VISIBLE: its path starts where the front machine's
12
+ edge crosses it (42,30), runs around the top, and stops at (28,22) where the front
13
+ machine covers it. Nothing is hidden, because nothing hidden was drawn.
14
+
15
+ Geometry in the 64 unit viewBox, with stroke-width 4 (so every edge overhangs by 2):
16
+ back screen x 28..54, y 10..30
17
+ back stand x 48, y 30..35 (offset right of centre: a centred stand
18
+ back foot x 43..53, y 35 would be covered by the front machine)
19
+ front screen x 10..42, y 22..46 (covers the back machine's lower left corner)
20
+ front stand x 26, y 46..52
21
+ front foot x 18..34, y 52
22
+ Span after the stroke: x 8..56, y 8..54, centred on (32,31) in the viewBox.
23
+
24
+ NO CONNECTING CABLE. An earlier revision drew a right angle off the lower left corner.
25
+ At 17 px it read as an unmotivated stray tail rather than a link, and the two feet
26
+ already say "two computers" without it. Removing it also stopped the composition being
27
+ pulled down and left.
28
+
29
+ XML HAZARD, and the reason this note is worded the way it is: a double hyphen is ILLEGAL
30
+ inside an XML comment. An earlier version of this file used one in three places, which
31
+ made the document malformed. The browser's img decoder then rejected it, the plugin
32
+ manager fell back to its default artwork, and the icon never appeared at all. Keep this
33
+ comment free of double hyphens.
34
+ -->
35
+ <g fill="none" stroke="#4D6BFE" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">
36
+ <!-- back machine: top, right, and only the visible part of its bottom and left edges -->
37
+ <path d="M42 30H54V10H28V22"/>
38
+ <!-- back machine's stand and foot, peeking out to the right -->
39
+ <path d="M48 30v5"/>
40
+ <path d="M43 35h10"/>
41
+ <!-- front machine -->
42
+ <path d="M10 22h32v24H10Z"/>
43
+ <!-- front machine's stand and foot -->
44
+ <path d="M26 46v6"/>
45
+ <path d="M18 52h16"/>
46
+ </g>
47
+ </svg>
@@ -0,0 +1,41 @@
1
+ # Bundle patch for dsh-plugin-remote-use.
2
+ #
3
+ # The row id is the plugin's stable identity in the Settings page and in the profile
4
+ # patch, so keep it stable across versions.
5
+ #
6
+ # ---------------------------------------------------------------------------
7
+ # NO TOOL ROW IS NEEDED, and this package deliberately has none.
8
+ #
9
+ # Earlier versions told the operator to add one `@deepseek-ai/dsh-tool-subagent` row per
10
+ # worker, with `provider: remote-<id>`. That instruction was WRONG, and following it ended
11
+ # with a configured worker, a restart, and still no tool for the model (PITFALLS F11).
12
+ #
13
+ # WHY: a Web session's model-facing tool catalog is enumerated by the AGENT PRESET
14
+ # (`preset-standard.plugins`), not by the loader tree. Such a row does compose -- the
15
+ # official `dsh --dump-config` shows it in the tree -- and still never reaches the model,
16
+ # because the preset does not list it. Registering a provider and exposing a tool are two
17
+ # different mechanisms, and only the second decides what the model can call.
18
+ #
19
+ # The plugin therefore registers `remote_subagent` itself through `ctx.tools.register()`,
20
+ # alongside its four `remote_*` observation tools. Adding a worker in the Settings page is
21
+ # the whole job: no profile edit, and no restart.
22
+ #
23
+ # `workers: []` here is only a default. The Settings page writes the real list to
24
+ # ~/.dsh/storages/dsh-plugin-remote-use/settings.json, which OVERRIDES this row.
25
+ # ---------------------------------------------------------------------------
26
+
27
+ # `id` is the ROW identity -- this plugin's stable name in the Settings page, and what a profile
28
+ # patch refers to when it overrides this row's config. It deliberately does NOT have to match the
29
+ # package name, and changing it would break every profile that already points at it.
30
+ #
31
+ # `name` is the MODULE SPECIFIER -- what the loader imports. It MUST be the package name exactly,
32
+ # scope included. These two were the same string while the package was unscoped, which is why
33
+ # renaming the package to `@hope_phenom/dsh-plugin-remote-use` silently broke the plugin: the
34
+ # manager still listed it (it reads package.json) and the toggle still moved, but the loader tried
35
+ # to import `dsh-plugin-remote-use`, found nothing under that name, and the server half never
36
+ # loaded -- so the Settings section never appeared.
37
+ - insert:
38
+ - id: dsh-plugin-remote-use
39
+ name: '@hope_phenom/dsh-plugin-remote-use'
40
+ config:
41
+ workers: []