dsh-remote 0.8.53 → 0.8.60

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/README.en.md CHANGED
@@ -277,6 +277,8 @@ Deploying to a product profile is a separate, explicit action (`./sync.sh`) for
277
277
  | `webInstallVersion` | string | `0.1.5-rc.2` | dsh version an automatic deployment installs; the default is the first that can start the web surface on Linux |
278
278
  | `webInstallRegistry` | string | `''` | npm registry for the install; **empty keeps the remote's own config**, so an internal mirror is never overridden |
279
279
  | `localTools` | string | `bootstrap` | which `rw_*` tools a LOCAL session (cwd outside every remote mirror) sees (issue #63): `bootstrap` keeps only the configuration loop `rw_machines`/`rw_connect`/`rw_pick_workspace` (the other 19 refuse a local session with "this session is LOCAL", so exposing them just wastes schema budget); `none` hides all `rw_*` (remote setup happens via the settings page only); `all` restores the legacy full surface. REMOTE sessions (cwd inside a mirror) always see everything; an invalid value degrades to `bootstrap` |
280
+ | `remoteSandbox` | string | `inherit` | which sandbox semantics remote operations follow (0.8.54+): `inherit` reads the mode the local `ctx.sandboxPolicy` resolved (read-only refuses remote writes/commands/forwards, workspace-write fences remote writes to the workspace root, danger-full-access allows everything); `off` disables the fence (legacy behaviour); an explicit mode name is also accepted. **This is containment, not a security boundary** — deploy hard boundaries on the remote side |
281
+ | `credentialFallback` | string | `refuse` | what to do when the OS secure store refuses a password (0.8.54+): `refuse` saves no password (the machine is still saved, the UI reports why, and nothing is written to machines.json in plaintext); `plain` restores the legacy plaintext fallback (with a warning). Turning encryption off explicitly (`backend: plain`) is unaffected |
280
282
 
281
283
  > The authoritative list is the `Config` schema in `lib/index.js`; this table mirrors it.
282
284
 
@@ -289,6 +291,26 @@ Two exceptions (0.8.52+): `.md`/`.markdown` links are **not** taken over and sta
289
291
 
290
292
  **One SFTP timeout wedged every SFTP tool on that machine** (issue #65) — fixed (0.8.52+). A single operation hitting the 20s timeout used to poison the whole process for that machine (every later SFTP call paid its own 20s, while `rw_exec` stayed fine) until a disconnect/reconnect. Now a timeout kills the channel so later requests fail fast; the native backend tears the connection down and re-handshakes after two consecutive timeouts, and the openssh backend rebuilds its sftp child — a transient network blip no longer ends with "disconnect and reconnect". As a bonus, `rw_edit` now writes atomically (temp file + rename), so an interrupted write can no longer truncate the remote file to zero bytes, and the original permission bits are preserved.
291
293
 
294
+ **Are remote operations subject to the local sandbox?** (0.8.54+) — yes. They were not before: a local session has `ctx.sandboxPolicy` (read-only / workspace-write / danger-full-access), while every `rw_*` tool ran remote operations with NO policy at all — so a read-only session could still mutate any file on any reachable host. The sandbox was not bypassed; it simply had no say over a different machine. The plugin now applies the same resolved mode at the TOOL boundary:
295
+
296
+ - `read-only` refuses remote writes, refuses running commands, refuses opening port forwards (reads and connections stay available, or the machine could not even be inspected);
297
+ - `workspace-write` requires remote writes to land inside the workspace root (a path fence) and allows commands and forwards;
298
+ - `danger-full-access` allows everything.
299
+
300
+ Why a tool-layer fence rather than `ctx.sandbox.confine()`: the latter wraps argv for a spawn on THIS machine, while our process runs on the far side of an SSH channel — the core docs say so themselves ("remote execution replaces the surrounding capability seam"). A denial therefore carries the OFFICIAL `FS_SANDBOX_DENIED` shape (with the `[sandbox: …]` marker and the escalation hint), so the model's escalation path still works.
301
+
302
+ **The threat model, stated plainly**: this is CONTAINMENT, not a security boundary (the same wording the official `dsh-fs-sandbox` header uses). Paths are compared lexically, so symlinks and TOCTOU inside the remote host can escape, and once bash runs it holds that user's own privileges. What it does buy: a confined session can no longer change remote files outside its workspace BY ACCIDENT, and a read-only session cannot quietly run commands. Deployments needing a hard boundary must confine on the REMOTE side (containers, per-user accounts, filesystem ACLs). Set `remoteSandbox: 'off'` to disable the fence.
303
+
304
+ **Does a password land in plaintext when the keychain fails?** (0.8.54+) — by default, no. Previously a failed secure-store write (locked keychain / missing secret-tool / Windows DPAPI without the assembly) fell back to PLAINTEXT in `machines.json` with a warning — you asked for encryption and got something less safe than if you had not asked, and a broken keychain is exactly when nobody notices. The default is now `credentialFallback: 'refuse'`: the password is NOT saved, the machine is still saved without a credential, and the UI reports the store failure. Set `credentialFallback: 'plain'` for the legacy behaviour. Turning encryption off explicitly (`backend: 'plain'`) is unaffected.
305
+
306
+ **How do I stop picking the wrong machine once I have many?** (0.8.54+) — give machines GROUP LABELS: `environment` (prod/staging…), `location` (shanghai/gpu-lab…), `description`, and `tags` (free-form). They are pure labels and change no connection behaviour. `rw_machines` filters on them (AND across dimensions, OR within a tag list): `rw_machines(environment="prod", tags="gpu")`. An empty result lists the groups that DO exist, so you never have to guess a spelling. The settings page edits them too. Past a dozen machines, "pick the right host" stops being a memory problem.
307
+
308
+ **What about multi-level bastions?** (0.8.54+) — **multi-hop chains** are supported (OpenSSH `ProxyJump a,b,c` semantics): a bastion may carry its own `proxy`, and the chain is built from the FARTHEST hop inward, each hop dialed through the already-open connection to the previous one. Two constraints matter: credentials do NOT inherit across hops (the target's private key is never presented to an intermediate host — it is usually not authorized there, and handing it over would leak a secret that host should never see), and a chain deeper than 8 hops is reported as an error, which turns a config CYCLE into a clear message instead of unbounded recursion. Each hop may carry its own `privateKeyPath` / `useAgent` / `hostKeyMode`.
309
+
310
+ **Can I open a remote terminal inside DSH?** (0.8.54+) — yes: an **embedded PTY terminal**. `rw_exec` runs one command and closes — no PTY, no stdin once the writer returns — so `vim`, `top`, a `sudo` prompt or any full-screen TUI is impossible. `pool.shell()` now opens a PERSISTENT PTY channel, and terminal sessions are host-owned: closing the browser does not kill the remote shell (a half-finished vim survives a reload), each session is reclaimed after 30 idle minutes, at most 8 live per process (so a UI cannot exhaust the remote `MaxSessions`), and unloading the plugin hangs up every one of them.
311
+
312
+ The transport is deliberately boring: the remote session lives on the HOST and the browser POLLS for output deltas (input is a separate request written immediately, so typing never waits for the poll cadence). That avoids depending on whether the host exposes a WebSocket upgrade path. The routes are `/dsh-remote/term/{open,input,output,resize,close,list}`, and each resolves the binding from the SESSION first — a terminal opened on host A can be neither seen nor driven by a request bound to host B.
313
+
292
314
  **The machine's IP / username changed — must every workspace be recreated?** (issue #67) — no (0.8.52+). Edit the machine's new address on the settings page and save, or call `rw_connect(machineId=…, host=…, username=…, port=…)` with the new address: every workspace mirror bound to that machine is **rebound in place** (its `.dsh-remote-meta.json` is rewritten) — local content is kept, no re-sync needed, and the save response says how many mirrors were rebound. The mirror directory name does not follow (live Sessions hold cwd paths inside it); the name is a display label only and does not affect connectivity.
293
315
 
294
316
  **Remote Sessions could not see the workspace AGENTS.md** (issue #68) — fixed (0.8.52+). The core instruction loader only knows the local filesystem, and a remote Session's cwd is the local mirror, so the remote repo's AGENTS.md never reached the model's context. Now, when `rw_pick_workspace` succeeds, the remote workspace gets a shallow scan (root + at most two levels down, matching `AGENTS.md`/`CLAUDE.md` only); the found files are synced into the local mirror under a three-way guard (root-level copies thus enter the core's own injection path, exactly like a local Session), and an index of paths + headings is rendered into the system prompt, telling the model to read nested instruction files with `rw_read_file` before editing. Full-tree recursion and plugin-side full-text injection are deliberately **not** done (the former would scan through multi-project roots; the latter would run a second injection pipeline in parallel with the core's).
@@ -333,6 +355,21 @@ Host devbox
333
355
 
334
356
  **How are passwords stored?** — tick "encrypt password": macOS Keychain / Windows DPAPI / Linux secret-tool (libsecret); falls back to plaintext when unavailable.
335
357
 
358
+ **Why is the password box empty (where are the stars)?** — No secret is ever sent back to the browser, so the input stays empty; when a password IS saved its placeholder reads
359
+ `•••••••• (saved — leave blank to keep)` with a hint underneath. Saving with an empty box keeps the stored password; typing a new value replaces it. Same for the private-key
360
+ passphrase and the jump-host password. The password badge in the machine row means exactly this (a keychain-stored password counts as saved).
361
+
362
+ **Which password does "Test connection" use?** — The form in front of you, but a blank password / key / passphrase **falls back to the value saved for that machine**, so
363
+ "edit a machine that already connects → click test" now works (the success line says the saved password was used). After saving, the form stays bound to that machine so you can
364
+ test immediately; press "Cancel edit" to start over.
365
+
366
+ **"No credentials" even though you filled it in** — the password never actually landed on disk: when "encrypt password" is ticked and the OS store is unavailable the plugin
367
+ **refuses** to write plaintext and reports a warning (`credentialFallback` defaults to `refuse`). Look for the red warning after saving, fix the store and save again, or use
368
+ "Test connection" to verify on the spot. A machine with no password / key / agent is flagged in amber right inside the form.
369
+
370
+ **"SSH agent unavailable"** — the machine has "use SSH agent" ticked but the DSH host process has no `SSH_AUTH_SOCK` (Windows has no agent service by default). Start an agent
371
+ (and export `SSH_AUTH_SOCK`, or use Pageant) before launching DSH — or untick it and save a password / private key instead.
372
+
336
373
  **The plugin vanished after a DSH upgrade** — DSH's compatibility check dropped the bundle; upgrade to **0.8.29+** (see "DSH version compatibility" above).
337
374
 
338
375
  **"Remote DSH Web UI" will not connect / spins forever** — first check that `dsh web` starts on that
package/README.md CHANGED
@@ -259,6 +259,8 @@ scripts/dev-run.sh --status # 是否在运行
259
259
  | `webInstallVersion` | string | `0.1.5-rc.2` | 自动部署安装的 dsh 版本;默认值是第一个在 Linux 上能启动 web 的版本 |
260
260
  | `webInstallRegistry` | string | `''` | 安装用的 npm 源;**留空沿用远端自己的配置**,不会覆盖内网镜像 |
261
261
  | `localTools` | string | `bootstrap` | 本地 session(cwd 不在远程镜像里)暴露哪些 `rw_*` 工具(issue #63):`bootstrap`=只暴露配置闭环 `rw_machines`/`rw_connect`/`rw_pick_workspace`(其余 19 个在本地 session 必然报 "this session is LOCAL",暴露只是白占 schema);`none`=完全不暴露 `rw_*`(远程配置只能走设置页);`all`=旧的 full 暴露。远程 session(cwd 在镜像里)不受影响,始终全量;非法值按 `bootstrap` 处理 |
262
+ | `remoteSandbox` | string | `inherit` | 远程操作套用哪套 sandbox 语义(0.8.54+):`inherit`=读取本机 `ctx.sandboxPolicy` 已解析的模式(read-only 拒远端写入/命令/转发,workspace-write 把远端写入围在 workspace 根内,danger-full-access 全放行);`off`=不设围栏(旧行为);也可显式写某一模式。**这是遏制不是安全边界**,硬边界请在远端一侧做 |
263
+ | `credentialFallback` | string | `refuse` | 系统密钥库写密码失败时怎么办(0.8.54+):`refuse`=不保存该密码(机器照常保存,界面报出失败原因,绝不把明文写进 machines.json);`plain`=旧的明文回退(会警告)。主动关掉加密(`backend: plain`)不受影响 |
262
264
 
263
265
  > 权威清单是 `lib/index.js` 里的 `Config` schema,本表与之一致。
264
266
 
@@ -284,6 +286,56 @@ scripts/dev-run.sh --status # 是否在运行
284
286
  一次网络抖动不再需要用「断开重连」来收场。顺带,`rw_edit` 改成了「临时文件 + rename」原子写,
285
287
  写一半超时不会再把远端文件截成 0 字节,原文件的权限位也会保留。
286
288
 
289
+ **远程操作受不受本机 sandbox 约束?**(0.8.54+)—— 受。以前不受:本机会话有 `ctx.sandboxPolicy`
290
+ (read-only / workspace-write / danger-full-access),而 `rw_*` 工具在远端执行**完全没有策略**,
291
+ 于是一个 read-only 会话照样能改任意一台可达机器上的任意文件——沙箱没有被绕过,它对另一台机器根本
292
+ 没有发言权。现在插件在**工具边界**套用同一套已解析的模式:
293
+
294
+ - `read-only`:拒绝远端写入、拒绝跑命令、拒绝开端口转发(读和连接仍可用,否则连机器都看不了);
295
+ - `workspace-write`:远端写入必须落在工作区根之内(按路径围栏),命令与转发放行;
296
+ - `danger-full-access`:全放行。
297
+
298
+ 为什么是工具层围栏而不是 `ctx.sandbox.confine()`:后者的语义是「把 argv 包起来在**本机** spawn」,
299
+ 而我们的进程跑在 SSH 通道的另一端——核心文档自己也写明「远端执行会**替换**周边 capability seam」。
300
+ 所以拒绝时抛出的是**官方同形状**的 `FS_SANDBOX_DENIED`(带 `[sandbox: …]` 与升级提示),模型的
301
+ 升级重试路径不受影响。
302
+
303
+ **威胁模型要讲清楚**:这是**遏制,不是安全边界**(与官方 `dsh-fs-sandbox` 文件头的口径一致)。
304
+ 路径按字典比较,远端机上的符号链接与 TOCTOU 可以绕过;一旦 bash 跑起来,它用的是该用户自己的权限。
305
+ 它真正买到的,是「受限会话不会**误**改工作区外的远端文件,且 read-only 会话不会偷偷跑命令」。
306
+ 需要硬边界的部署,请在**远端那一侧**做(容器、按用户隔离、文件系统 ACL)。
307
+ 不想用可设 `remoteSandbox: 'off'`(或 `'inherit'` 之外的值显式指定某一模式)。
308
+
309
+ **密钥库写不进去时,密码会明文落盘吗?**(0.8.54+)—— 默认**不会**。以前系统密钥库失败
310
+ (keychain 锁着 / 缺 secret-tool / Windows DPAPI 缺程序集)会**回退明文**写进 `machines.json` 并
311
+ 给个警告——你本来要的是加密,结果反而更不安全,而「密钥库正好坏了」恰恰是最不容易注意到的时候。
312
+ 现在默认 `credentialFallback: 'refuse'`:**不保存**该密码,机器照常保存但没有凭据,界面明确报出
313
+ 密钥库失败原因。想要旧行为(宁可明文也要能连)就设 `credentialFallback: 'plain'`。
314
+ 主动关掉加密(`backend: 'plain'`)不受影响,那本来就是明文。
315
+
316
+ **机器多了怎么不选错?**(0.8.54+)—— 给机器打**分组标签**:`environment`(prod/staging…)、
317
+ `location`(shanghai/gpu-lab…)、`description`、`tags`(一组自由标签)。它们纯粹是标签,不改变任何
318
+ 连接行为。`rw_machines` 支持按这些维度筛选(维度之间 AND,标签之间 OR):
319
+ `rw_machines(environment="prod", tags="gpu")`。筛不到时会列出**当前有哪些分组**,省得你猜标签拼法。
320
+ 设置页同样可以编辑。机器一多,「选对机器」就从记性变成了筛选。
321
+
322
+ **多级跳板机怎么办?**(0.8.54+)—— 支持**多跳链路**(等价 OpenSSH 的 `ProxyJump a,b,c`):
323
+ 跳板机自己也可以带 `proxy`,链路从**最远一跳**往里建,每一跳都通过上一跳已建立的连接拨号。
324
+ 两个关键约束:**凭据不跨跳继承**(绝不把目标机的私钥递给中间机——既通常没被授权,也等于把不该给的
325
+ 秘密给了它);链路深度超过 8 跳会明确报错,用来把**配置成环**变成一条清晰的错误,而不是无限递归。
326
+ 每一跳可以自带 `privateKeyPath` / `useAgent` / `hostKeyMode`,没写就沿用操作者自己的 ssh 环境。
327
+
328
+ **能在 DSH 里直接开一个远端终端吗?**(0.8.54+)—— 能,**内嵌 PTY 终端**。`rw_exec` 是「跑一条命令
329
+ 就关」,没有 PTY、stdin 也回不去,所以 `vim`、`top`、`sudo` 提示符、全屏 TUI 都用不了。
330
+ 现在 `pool.shell()` 打开一条**持久的 PTY 通道**,终端会话归宿主所有:关掉浏览器不会杀掉远端 shell
331
+ (写了一半的 vim 能活过刷新),每个会话有 30 分钟空闲回收、进程内最多 8 个(免得打爆远端
332
+ `MaxSessions`),插件卸载时全部挂断。
333
+
334
+ 传输方式刻意做得很朴素:远端会话在**宿主**,浏览器**轮询**输出增量(输入是单独请求、立即写入,
335
+ 所以打字从不排队等轮询)。这样不依赖宿主是否暴露 WebSocket 升级路径。路由是
336
+ `/dsh-remote/term/{open,input,output,resize,close,list}`,每个都先按**会话**解析绑定——
337
+ 开在 A 机的终端,绑到 B 机的请求既看不到也驱动不了。
338
+
287
339
  **机器换了 IP / 用户名,工作区要全部重建吗?**(issue #67)—— 不用(0.8.52+)。在设置页编辑该机器的新
288
340
  地址并保存,或 `rw_connect(machineId=…, host=…, username=…, port=…)` 同传新地址:该机器名下所有工作区
289
341
  镜像会**原地重绑**(改写镜像的 `.dsh-remote-meta.json`),本地内容保留、无需重新同步,保存后会提示重绑了
@@ -374,6 +426,19 @@ Host devbox
374
426
 
375
427
  **密码怎么加密保存** —— 勾选「加密保存密码」:macOS 钥匙串 / Windows DPAPI / Linux secret-tool(libsecret);后端不可用时回退明文。
376
428
 
429
+ **密码框为什么是空的(星号在哪)** —— 出于安全,**任何密码都不会回传到浏览器**,所以输入框永远是空的;已经保存过密码时,输入框的占位符会显示
430
+ `••••••••(已保存,留空 = 不修改)`,下方还有一行绿字说明。留空保存 = 沿用已保存的密码,填新值 = 覆盖。passphrase 与跳板机密码同理。
431
+ 机器列表里的「密码」标签也是这个含义(钥匙串里的密码同样算已保存)。
432
+
433
+ **「测试连接」用哪个密码** —— 用你眼前这份表单,但**留空的密码/私钥/passphrase 会自动沿用该机器已保存的值**,所以「编辑一台已经连上的机器 → 直接点测试」是能过的
434
+ (成功提示会注明「本次测试使用了已保存的密码」)。保存之后表单会**留在该机器上**,可以立刻点测试验证;想清空重来点「取消编辑」。
435
+
436
+ **报「缺少凭据」但你确信填过** —— 说明保存时密码没有真正落盘:勾了「加密保存密码」而系统钥匙库不可用时,插件会**拒绝**把密码写成明文并给出警告
437
+ (`credentialFallback` 默认 `refuse`)。看保存后有没有红色警告,按提示修好钥匙库再存一次,或用「测试连接」当场验证。机器没有密码/私钥/agent 时,编辑表单里会直接标黄提醒。
438
+
439
+ **报「SSH agent 不可用」** —— 该机器勾了「使用 SSH agent」,但 DSH 宿主进程里没有 `SSH_AUTH_SOCK`(Windows 默认没有 agent 服务)。启动 DSH 之前把 agent 跑起来并导出
440
+ `SSH_AUTH_SOCK`(或用 Pageant),或者取消勾选、改用密码/私钥。
441
+
377
442
  **升级后插件整个不见了** —— DSH 兼容性判定丢弃了 bundle,升到 **0.8.29+** 即可(见上文「DSH 版本兼容性」)。
378
443
 
379
444
  **「远程 DSH 界面」连不上/一直转圈** —— 先确认那台机器上的 `dsh web` **本身**能起来。