dsh-ssh-workbench 0.0.0-stage → 1.7.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 +552 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/README.md +178 -2
- package/SECURITY.md +27 -0
- package/cordis.patch.yml +5 -0
- package/docs/architecture.md +146 -0
- package/docs/execution-model.md +83 -0
- package/docs/images/chat-at-resource.png +0 -0
- package/docs/images/plugin.png +0 -0
- package/docs/images/remote-workbench.png +0 -0
- package/docs/images/workbench.png +0 -0
- package/docs/quickstart.md +106 -0
- package/lib/client.js +4991 -0
- package/lib/core/activity.js +88 -0
- package/lib/core/background.js +69 -0
- package/lib/core/broker.js +802 -0
- package/lib/core/dialect.js +28 -0
- package/lib/core/errors.js +138 -0
- package/lib/core/execution-stats.js +88 -0
- package/lib/core/gate-pwsh.js +88 -0
- package/lib/core/gate.js +572 -0
- package/lib/core/guard.js +123 -0
- package/lib/core/known-hosts.js +74 -0
- package/lib/core/live-executions.js +101 -0
- package/lib/core/local-file-api.js +174 -0
- package/lib/core/localfs.js +381 -0
- package/lib/core/managed-context.js +276 -0
- package/lib/core/mentions.js +216 -0
- package/lib/core/paths.js +104 -0
- package/lib/core/posix-quote.js +4 -0
- package/lib/core/posix-shell.js +89 -0
- package/lib/core/prefs.js +38 -0
- package/lib/core/remote-exec-tool.js +394 -0
- package/lib/core/remote-file-tools.js +559 -0
- package/lib/core/remote-shell-tool.js +310 -0
- package/lib/core/secrets.js +95 -0
- package/lib/core/server-ref.js +61 -0
- package/lib/core/shell-adapters.js +48 -0
- package/lib/core/ssh.js +761 -0
- package/lib/core/store.js +83 -0
- package/lib/core/terminals.js +299 -0
- package/lib/core/win32-adapter.js +141 -0
- package/lib/index.js +409 -0
- package/lib/routes.js +759 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +89 -4
- package/vendor/NOTICE.md +50 -0
- package/vendor/XTERM-LICENSE +21 -0
- package/vendor/node_modules/asn1/LICENSE +19 -0
- package/vendor/node_modules/asn1/README.md +50 -0
- package/vendor/node_modules/asn1/lib/ber/errors.js +13 -0
- package/vendor/node_modules/asn1/lib/ber/index.js +27 -0
- package/vendor/node_modules/asn1/lib/ber/reader.js +262 -0
- package/vendor/node_modules/asn1/lib/ber/types.js +36 -0
- package/vendor/node_modules/asn1/lib/ber/writer.js +317 -0
- package/vendor/node_modules/asn1/lib/index.js +20 -0
- package/vendor/node_modules/asn1/package.json +31 -0
- package/vendor/node_modules/bcrypt-pbkdf/CONTRIBUTING.md +13 -0
- package/vendor/node_modules/bcrypt-pbkdf/LICENSE +66 -0
- package/vendor/node_modules/bcrypt-pbkdf/README.md +45 -0
- package/vendor/node_modules/bcrypt-pbkdf/index.js +556 -0
- package/vendor/node_modules/bcrypt-pbkdf/package.json +15 -0
- package/vendor/node_modules/safer-buffer/LICENSE +21 -0
- package/vendor/node_modules/safer-buffer/Readme.md +156 -0
- package/vendor/node_modules/safer-buffer/dangerous.js +58 -0
- package/vendor/node_modules/safer-buffer/package.json +34 -0
- package/vendor/node_modules/safer-buffer/safer.js +77 -0
- package/vendor/node_modules/tweetnacl/AUTHORS.md +28 -0
- package/vendor/node_modules/tweetnacl/LICENSE +24 -0
- package/vendor/node_modules/tweetnacl/README.md +459 -0
- package/vendor/node_modules/tweetnacl/nacl-fast.js +2388 -0
- package/vendor/node_modules/tweetnacl/nacl.js +1175 -0
- package/vendor/node_modules/tweetnacl/package.json +58 -0
- package/vendor/ssh2/LICENSE +19 -0
- package/vendor/ssh2/lib/Channel.js +295 -0
- package/vendor/ssh2/lib/agent.js +1123 -0
- package/vendor/ssh2/lib/client.js +2176 -0
- package/vendor/ssh2/lib/http-agents.js +84 -0
- package/vendor/ssh2/lib/index.js +44 -0
- package/vendor/ssh2/lib/keygen.js +582 -0
- package/vendor/ssh2/lib/protocol/Protocol.js +2136 -0
- package/vendor/ssh2/lib/protocol/SFTP.js +4052 -0
- package/vendor/ssh2/lib/protocol/constants.js +356 -0
- package/vendor/ssh2/lib/protocol/crypto/poly1305.js +43 -0
- package/vendor/ssh2/lib/protocol/crypto.js +1602 -0
- package/vendor/ssh2/lib/protocol/handlers.js +16 -0
- package/vendor/ssh2/lib/protocol/handlers.misc.js +1285 -0
- package/vendor/ssh2/lib/protocol/kex.js +1908 -0
- package/vendor/ssh2/lib/protocol/keyParser.js +1484 -0
- package/vendor/ssh2/lib/protocol/node-fs-compat.js +115 -0
- package/vendor/ssh2/lib/protocol/utils.js +356 -0
- package/vendor/ssh2/lib/protocol/zlib.js +255 -0
- package/vendor/ssh2/lib/server.js +1380 -0
- package/vendor/ssh2/lib/utils.js +336 -0
- package/vendor/ssh2/package.json +34 -0
- package/vendor/xterm.js +53 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# 贡献指南
|
|
2
|
+
|
|
3
|
+
欢迎参与 **SSH Workbench**(中文名「远程工作台」,仓库/包名 `dsh-ssh-workbench`)的开发。
|
|
4
|
+
|
|
5
|
+
## 开发环境
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone <repo>
|
|
9
|
+
cd dsh-ssh-workbench
|
|
10
|
+
npm test # 单元测试(不需要真实 SSH 服务器)
|
|
11
|
+
npm run test:ssh # SSH 传输集成测试(进程内 ssh2.Server)
|
|
12
|
+
npm run test:ssh:sftp # SFTP 集成测试
|
|
13
|
+
npm run pack # 生成 dist/dsh-ssh-workbench-<version>.tgz
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
要求:Node.js 20+(仓库在 Node 26 上验证),npm。单元测试使用本地 stub;集成测试会启动进程内 ssh2 服务器,使用仓库 `vendor/ssh2` 里的副本(可用 `SDH_SSH2_PATH` 覆盖)。
|
|
17
|
+
|
|
18
|
+
## 目录结构
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
lib/index.js host 半:插件入口、Config、工具注册(remote_bash / remote_pwsh / remote_exec / remote_* 文件工具)
|
|
22
|
+
lib/routes.js UI 数据面:资源、凭据、连接测试、SFTP、终端、偏好
|
|
23
|
+
lib/client.js client 半:工作台、设置页、侧栏面板、工具卡片、i18n
|
|
24
|
+
lib/core/ 传输与领域模块:broker/ssh/dialect/shell-adapters/posix-shell/
|
|
25
|
+
remote-shell-tool/remote-exec-tool/remote-file-tools/managed-context/
|
|
26
|
+
mentions/execution-stats/live-executions/background/win32-adapter/
|
|
27
|
+
gate(-pwsh)/guard/store/secrets/known-hosts/activity/terminals/localfs
|
|
28
|
+
test/ 单元测试、集成测试、基准脚本、真机回归脚本
|
|
29
|
+
docs/ 用户文档(docs/quickstart.md、docs/architecture.md)
|
|
30
|
+
vendor/ 随包携带的纯 JS 依赖(ssh2 及 xterm 构建输入);版本/许可证见 vendor/NOTICE.md
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 依赖约定
|
|
34
|
+
|
|
35
|
+
- 发布包必须保持**零安装运行时依赖**:安装插件不运行 `node-gyp`、不下载 npm 包。需要新增纯 JS 依赖时,把它放到 `vendor/` 并在 `vendor/NOTICE.md` 登记版本与许可证,而不是加进 `dependencies`;原生依赖不要引入。
|
|
36
|
+
- `vendor/ssh2` 来自 ssh2 1.17.0,升级步骤见 `vendor/NOTICE.md`;升级后必须跑三套测试(`npm test`、`npm run test:ssh`、`npm run test:ssh:sftp`)。
|
|
37
|
+
- **后续所有版本都必须是自包含包**:不新增 `dependencies` / `optionalDependencies`,不引入安装期脚本或原生编译。
|
|
38
|
+
|
|
39
|
+
## 发布检查
|
|
40
|
+
|
|
41
|
+
每个版本发布前:
|
|
42
|
+
|
|
43
|
+
1. 确认 `package.json` 没有 `dependencies` / `optionalDependencies`,`vendor/` 与 `vendor/NOTICE.md` 完整,`npm test` 里的自包含守卫通过;
|
|
44
|
+
2. 跑 `npm test`、`npm run test:ssh`、`npm run test:ssh:sftp`;
|
|
45
|
+
3. `npm run pack` 后检查 `dist/*.tgz`:包内包含 `vendor/ssh2` 与 `vendor/node_modules`,没有 `install.js`、`binding.gyp`、`.node`;
|
|
46
|
+
4. 在干净目录用 DSH 自带 pnpm 执行 `pnpm add <tgz> --offline`,确认没有下载/编译且插件可加载;
|
|
47
|
+
5. 安装到本机 DSH profile,重启后确认版本与功能正常,再提交、打 tag、发布 Release。
|
|
48
|
+
|
|
49
|
+
## 编码约定
|
|
50
|
+
|
|
51
|
+
- 保持插件是普通 DSH bundle:不 patch 宿主、不另造审批系统。
|
|
52
|
+
- 远程命令的安全兜底是 `ctx.tools.guard()` 的机器级黑名单(POSIX 与 PowerShell 两套规则),其余审批跟随 DSH 会话预设。
|
|
53
|
+
- 工具输出必须有界(输出上限/截断)且可读;新的远程命令工具走 shell adapter,不在工具体内直接拼方言命令。
|
|
54
|
+
- UI 使用 DSH 设计令牌与官方组件;所有用户可见文案在 `lib/client.js` 里补中英文(zh/en key 必须成对)。
|
|
55
|
+
- 优先把纯逻辑放进 `lib/core/`,保持可单测;新增方言/传输行为先补测试。
|
|
56
|
+
|
|
57
|
+
## 提交 Pull Request
|
|
58
|
+
|
|
59
|
+
1. 行为变更需要补/改测试;
|
|
60
|
+
2. 运行 `npm test`(涉及传输时再跑集成测试);
|
|
61
|
+
3. 用户可见行为变化时更新 `README.md` / `CHANGELOG.md`;
|
|
62
|
+
4. 提交保持聚焦,说明用户可见的效果;
|
|
63
|
+
5. 历史细节写进 `CHANGELOG.md`,不要往维护中文档里堆旧版本内容或过程性报告。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 duobaobox
|
|
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,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SSH Workbench(dsh-ssh-workbench)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness 的**远程工作台**:SSH 终端、本地与远程文件、@服务器 对话执行,Windows 宿主和目标都能用。
|
|
4
|
+
|
|
5
|
+
不是又一个 SSH 客户端:它把远程执行接进 DSH 的对话与审批,把终端、SFTP 文件管理和本地文件放在同一个工作台里;安装包自包含、开箱即用。
|
|
6
|
+
|
|
7
|
+
插件不改 DSH 的本地工具语义;左侧导航提供一个独立的「远程工作台」,把终端与文件管理放在一起(本地和远程都能用)。**所有远程执行都显式带目标**:原生 `bash` / `pwsh` 只在本机生效;对话里 `@资源名` 只是告诉模型“这轮可能用哪台”,模型实际调用 `remote_bash({ server, command })`、`remote_pwsh({ server, command })`、`remote_exec({ tasks })`,必要时用 `remote_read` / `remote_glob` / `remote_grep` 查远程文件。没有会话级 active target,也没有“切回本地”的状态残留:多轮对话里目标不会静默漂移。另外保留一道**危险命令黑名单**作为兜底。
|
|
8
|
+
|
|
9
|
+
- 适用:DSH 0.2.1-rc.1 ~ 0.2.x(`engines.dsh: >=0.2.1-rc.1 <0.3.0`)
|
|
10
|
+
- 平台:macOS / Linux / Windows 上的 DSH;远程目标支持 POSIX 与 Windows(PowerShell)
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## 能力
|
|
17
|
+
|
|
18
|
+
| 能力 | 说明 |
|
|
19
|
+
|---|---|
|
|
20
|
+
| 远程资源纳管 | 右侧栏/设置页:增删改查、连接测试、host key 指纹确认、密码/私钥凭据(0600 存储);连接测试/选中目标后会采集 hostname 等信息(hostname 仅用于目标上下文;内核/磁盘只存储,不在列表展示) |
|
|
21
|
+
| `@资源` 提示 | 消息里 `@web-1` 会让模型在工具调用里带上 `server: "web-1"`(资源名支持中文/非 ASCII);`@` 多个资源时用 `remote_exec` 一次并行执行。`@` 只是本轮的提示,不是全局 active target;模型仍可按上下文显式选择其他资源 |
|
|
22
|
+
| 执行目标 | 每个远程工具调用自己带 `server`:`remote_bash({ server, command })`、`remote_pwsh({ server, command })`、`remote_exec({ tasks: [{ server, command }] })`。不存在“当前目标”,因此切回本机只是改调原生 `bash` / `pwsh`,不需要 `ssh_workbench_target`,也不会把本地命令误发到远程 |
|
|
23
|
+
| Windows 宿主 | Windows 上的 DSH 没有原生 `bash`(DSH 按平台禁用):插件在两种宿主上都注册 `remote_bash` / `remote_pwsh` / `remote_exec`,所以 Windows 宿主也能操作 POSIX 目标;原生 `pwsh` 仍只作用于本机 |
|
|
24
|
+
| Windows 目标 | Windows 目标经插件自有的 `remote_pwsh` 执行:命令包成 UTF-16LE 的 `-EncodedCommand` 脚本(避免引号转义),强制 UTF-8 输出并回传真实退出码;PowerShell 危险命令另有黑名单 |
|
|
25
|
+
| 方言探测 | 目标实际是 POSIX 还是 Windows 由连接后的 `uname -s` / `cmd /c ver` 探测并写入 `facts.dialect`,不依赖手工填写的 `platform`;探测结果用于选择 `remote_bash` / `remote_pwsh`、超时包装和目标上下文文案 |
|
|
26
|
+
| 远程文件工具 | `remote_read` / `remote_glob` / `remote_grep` 直接操作远程文件(显式 `server`);`remote_write` / `remote_edit` 默认关闭,需开启 `remoteFileWrite`。原生 `read`/`write`/`edit`/`glob`/`grep` 仍只作用于本地 |
|
|
27
|
+
| `remote_exec` 多资源并行 | 一次调用传 `tasks: [{ server, command, workdir?, timeoutMs? }]`;每个任务**显式指定目标**,按各自方言选择 POSIX/Windows 执行器,并发执行(默认 4,上限 8),逐任务超时、输出上限与错误隔离;不依赖也不修改任何会话目标状态。仅前台执行,长任务仍用后台工具 |
|
|
28
|
+
| `remote_bash` / `remote_pwsh` | `command`、`workdir`(绝对路径 / `~/x` / 相对路径)、`timeoutMs`、取消沿用同一套语义;POSIX 目标超时后远端命令组会被终止(远端有 `timeout` 时含 `-k` 补 KILL),Windows 目标只能客户端停止等待 |
|
|
29
|
+
| 结果输出 | 原生 `bash` / `pwsh` 保留 DSH 原生终端卡片;`remote_bash` / `remote_pwsh` / `remote_exec` 返回带 server / cwd / 退出码 / stdout / stderr 的文本结果 |
|
|
30
|
+
| 后台任务 | `run_in_background: true` 在远端起**脱离会话**的进程:POSIX 用 `nohup` 重定向到 `/tmp` 日志,Windows 用 `Start-Process` + 日志文件;结果返回 pid、日志路径和读取/停止命令(`tail -f` / `kill`、`Get-Content -Wait` / `Stop-Process`)。它不注册到 DSH 本地 `job_*` 注册表,进程在工具调用返回后继续运行 |
|
|
31
|
+
| 平台徽标 | 资源列表/设置页显示 **Linux / Windows** 徽标(来自连接后探测的 `facts.dialect`,不是手填字段);连接测试结果也显示探测到的平台 |
|
|
32
|
+
| 执行统计 | composer 下方的 pill 只读展示本会话的远程执行次数,结构对齐 DSH Token 用量弹窗:标题「远程执行」+ 总次数、分隔线、目标名 → 次数;没有执行时不显示。执行中 pill 会实时显示当前目标(单目标为 `host165 · 执行中`,多目标为 `2 个目标 · 执行中`),1.5 秒轮询;它不切换目标、不打开工作台,也不显示成功/失败或用时 |
|
|
33
|
+
| 远程工作台 | 左侧导航的独立页面:左侧终端(本地终端 / 远程 SSH 终端),右侧文件管理(本地用户主目录 / 远程 SFTP,远程默认打开登录 home),文件可**在工作台内直接预览**,拖动分隔条调整宽度;每个页签一个终端会话,仅活动页签轮询、非活动页签暂停并在切回时续读(UI 能力,不占模型工具) |
|
|
34
|
+
| 侧边栏开关 | 插件管理 → 远程工作台 → 页面里的「在侧边栏显示远程工作台」可关闭左侧导航入口;关闭后 `@资源名` 远程执行、设置页与模型工具不受影响 |
|
|
35
|
+
| 危险命令黑名单 | `rm -rf /`、`mkfs`、`dd of=/dev/*`、`fdisk/parted/wipefs`、改根目录权限、fork bomb 等同步拦截;多行脚本、`&`、引号/转义、路径变体、`$()`/`eval` 都会先还原再判断;其他命令交给 DSH 审批 |
|
|
36
|
+
|
|
37
|
+
**不做**:诊断包、HTTP 探测、跨主机文件分发、远程 IDE/SSHFS/透明代理(远程文件能力是显式的 `remote_*` 工具,不把本地 `read`/`write` 隐式重定向到远端)、命令分级(allow/guarded)、AI 风险解释和审计。
|
|
38
|
+
|
|
39
|
+
## 安装
|
|
40
|
+
|
|
41
|
+
DSH 插件管理里搜索 `dsh-ssh-workbench` 安装,或:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
dsh plugin --profile desktop add dsh-ssh-workbench
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
从源码构建:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm test
|
|
51
|
+
npm run pack
|
|
52
|
+
# 然后在 DSH 的插件管理里安装 dist/dsh-ssh-workbench-1.7.0.tgz
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
发布包是**自包含**的:SSH 传输依赖(纯 JS 的 ssh2 及其依赖)已放在 `vendor/` 里随包携带,安装时不会运行 `node-gyp`、不需要 Xcode Command Line Tools,也不会额外下载依赖;包体积因此略大(约 0.5 MB),换来任何机器开箱即装。组件与许可证见 `vendor/NOTICE.md`。
|
|
56
|
+
|
|
57
|
+
本地开发时,也可以在 DSH 的插件管理页直接安装本目录(Bundle 安装会读取 `package.json` 的 `dsh` 字段)。
|
|
58
|
+
|
|
59
|
+
安装后在 **设置 → 远程工作台** 里添加资源。
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
## 用法
|
|
64
|
+
|
|
65
|
+
1. 先说目标,再说要做什么:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
@web-1 看下磁盘和内存
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
模型会调用 `remote_bash({ server: "web-1", command })`(Windows 目标是 `remote_pwsh`),命令实际跑在目标上。`read` / `write` / `edit` / `glob` / `grep` 仍然作用于本机;要读写远程文件,模型会改用 `remote_read` / `remote_glob` / `remote_grep`(写入需开启 `remoteFileWrite`):
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
@web-1 把 /etc/nginx/nginx.conf 里 worker_connections 改成 2048,改完 nginx -t
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
2. 同时提到多个资源时,让模型用 `remote_exec` 一次并行执行,不必逐个切换:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
@web-1 @db-1 各自看下磁盘
|
|
81
|
+
→ 模型调用 remote_exec({ tasks: [
|
|
82
|
+
{ server: "web-1", command: "df -h" },
|
|
83
|
+
{ server: "db-1", command: "df -h" }
|
|
84
|
+
] })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
每个 task 自带目标,命令在各自服务器上并发执行,结果按任务顺序返回到同一条工具卡片;不读取也不改变任何会话目标状态。composer 下方的 pill 只读展示本会话执行次数,点开是按目标列出的计数卡;不负责切换或打开工作台。
|
|
88
|
+
|
|
89
|
+
3. 切回本机:直接让模型用原生 `bash` / `pwsh`,不需要任何“切目标”动作;远程调用只有显式带 `server` 才会发出。
|
|
90
|
+
|
|
91
|
+
## 安全模型
|
|
92
|
+
|
|
93
|
+
- **黑名单兜底**:`remote_bash` / `remote_pwsh` / `remote_exec` 执行前经过同步、单调的 `ctx.tools.guard()`:每个显式 `server`(以及 `remote_exec` 的每个 task)都会按对应方言检查,命中机器毁灭类命令直接拒绝;该拦截不受审批策略影响。原生 `bash` / `pwsh` 是本地工具,不经过远程黑名单。POSIX 规则先按 shell 语义还原(去引号/转义、剥包装、路径规范化、`$()`/`eval` 递归)再匹配,PowerShell 规则覆盖 `Format-Volume`、`Clear-Disk`、`diskpart`、`bcdedit`、整盘递归删除等;两者都只是 best-effort。
|
|
94
|
+
- **审批**:插件不做自己的 ask 清单,远程命令是否弹审批完全由 DSH 的会话预设(`ask` / `never`)决定。
|
|
95
|
+
- **凭据**:密码存于 `~/.dsh/ssh-workbench/`(0600 文件 + 本机密钥),私钥只存路径;servers.json 0600。
|
|
96
|
+
- **主机身份**:严格校验 known_hosts;未知指纹会显示 SHA256 并询问是否信任,由用户决定;信任后写入插件自己的 known_hosts。**如果服务器重装或更换导致指纹变化,连接会被拒绝并提示「服务器主机密钥已变化」**,核对后可在界面点「更新信任」。
|
|
97
|
+
- **本地文件面板**:本地文件浏览/读写/删除默认在本地用户主目录内进行(可用 `localRoot` 改到其他绝对路径);指向目录外的符号链接与越界路径一律拒绝,删除链接只删除链接本身。
|
|
98
|
+
- **注意**:远程命令在服务器上执行,不受 DSH 本地文件沙箱约束;安全边界是服务器自身的权限、主机身份校验和黑名单。
|
|
99
|
+
|
|
100
|
+
## 已知限制
|
|
101
|
+
|
|
102
|
+
- **stdout/stderr 顺序**:两者是独立管道,收集后分开返回(与原生 bash 的值契约一致),交错顺序会丢失;看 `make`/`pip install` 这类日志时需要留意。
|
|
103
|
+
- **Windows 本地路径**:工作台本地文件面板已按 Windows 分隔符处理面包屑与文件名;极少数外部工具生成的混合分隔符路径仍可能显示成一段,可在路径编辑框直接输入绝对路径。
|
|
104
|
+
- **`remote_exec` 仅前台**:不支持 `run_in_background`;一次最多 8 个 task、默认并发 4,所有结果共享 `maxBatchOutputBytes` 预算,超出会按任务截断。长时间任务仍应分别用后台工具或终端。
|
|
105
|
+
- **后台任务**:`run_in_background` 返回的是远端 pid + 日志,不是 DSH 本地 job id;`timeoutMs` 和取消只影响启动调用,不作用于已脱离的进程。POSIX 的 `/tmp` 日志可能被系统清理(按 tmp 策略),重要输出请自行重定向到持久目录。
|
|
106
|
+
- **超时终止**:远端有 `timeout` 时,超时会连同远端进程组一起终止(支持 `-k` 时再补一次 KILL,缓冲时间可用 `timeoutKillAfterMs` 调整);远端没有 `timeout` 时只能停止等待,stderr 会提示"命令可能仍在运行"。**取消(Esc)不会立刻杀远端**:通道关闭后远端仍会计时到自己的 deadline。
|
|
107
|
+
- **stdin 恒为 EOF**:远程命令的 stdin 在启动后立即关闭(等价 `</dev/null`),`cat`/`read` 等读 stdin 的命令会直接得到 EOF。
|
|
108
|
+
- **文件工具分本地/远程**:原生 `read`/`write`/`edit`/`glob`/`grep` 只作用于本地;远程文件用显式的 `remote_read`/`remote_glob`/`remote_grep`,写入用 `remote_write`/`remote_edit`(默认关闭)。远程写入不是原子操作,修改配置前请备份;大文件/二进制仍建议走右侧栏 SFTP 面板。
|
|
109
|
+
- **并发由 DSH 调度**:`remote_exec` 显式声明并行安全,因此一次批量任务会真正并发(受 `batchConcurrency` 上限约束);原生 `bash` 与 `remote_bash` / `remote_pwsh` 仍是 exclusive 工具,同一轮里的多个 shell 调用按 DSH 调度串行。插件为单台服务器提供最多 6 条并发通道。
|
|
110
|
+
- **目标标识**:DSH 的工具卡片是官方渲染且没有装饰插槽,插件不替换卡片;远程调用的目标由工具参数(`server`)与结果里的 `target` / `cwd` 行体现,composer 下方的 pill 另提供本会话执行次数与执行中的实时目标。
|
|
111
|
+
- **侧边栏开关**:开关存放在 `~/.dsh/ssh-workbench/prefs.json`,在插件管理里保存后当前窗口立即生效;关闭时左侧导航不再出现远程工作台入口,右侧栏的「远程工作台」「远程文件」页签保持不变。
|
|
112
|
+
- **终端生命周期**:终端会话跟随工作台页面/页签,关闭页签或离开工作台会结束会话;页面刷新/关闭时通过 `sendBeacon` 尽力补发关闭,但进程崩溃或 Host 异常退出后仍依赖 30 分钟空闲回收(尚未做跨页面保活或重连恢复)。本地终端依赖 Host 的 PTY 服务,不可用时给出提示。
|
|
113
|
+
- **本地文件边界**:工作台的本地文件面板与本地终端默认以**本地用户主目录**为根(`$HOME`,与常规 SSH 客户端的「本地」面板一致,面包屑显示真实路径),越界路径与指向外部的符号链接都会被拒绝;需要固定其他目录时用 `localRoot` 配置(绝对路径或 `~/...`),需要操作主目录以外的本地路径时用本地终端。远程文件仍走 SFTP,远程文件面板默认打开登录 home。
|
|
114
|
+
- **终端渲染资源**:xterm 随客户端 bundle 内置(`vendor/xterm.js` 只是构建输入,不再有运行时脚本路由),不依赖额外脚本路由或 CSP 放行;首次打开工作台时加载。
|
|
115
|
+
- **终端操作边界**:本地/远程终端是用户主动输入,不经过 agent 的审批与黑名单;SSH 权限边界不变。
|
|
116
|
+
- **黑名单是 best-effort**:`bash -c`、base64、脚本文件、解释器 heredoc 正文不会被解析;heredoc 正文会按可执行文本检查(fail-closed,写脚本时可能误拦)。`remote_exec` 会对每个 task 按自己目标方言逐条过 guard,但仍是 best-effort。真正的边界是 SSH 最小权限 + host-key + DSH 审批。
|
|
117
|
+
|
|
118
|
+
## 配置
|
|
119
|
+
|
|
120
|
+
在 profile 的 `cordis.patch.yml` 里覆盖(只写想改的字段):
|
|
121
|
+
|
|
122
|
+
| 键 | 默认 | 说明 |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `commandTimeoutMs` | `120000` | 单条远程命令默认超时;模型可用 bash 的 `timeoutMs` 覆盖 |
|
|
125
|
+
| `maxCommandTimeoutMs` | `1800000` | 单条远程命令超时上限 |
|
|
126
|
+
| `timeoutKillAfterMs` | `5000` | 远端 `timeout -k` 的宽限:TERM 后多久补 SIGKILL;0 = 只用 TERM |
|
|
127
|
+
| `maxOutputBytes` | `102400` | stdout 保留上限(头尾保留,中间截断) |
|
|
128
|
+
| `maxStderrBytes` | `16384` | stderr 独立保留上限(默认明显小于 stdout,保护上下文) |
|
|
129
|
+
| `maxBatchTasks` | `8` | 一次 `remote_exec` 最多任务数 |
|
|
130
|
+
| `batchConcurrency` | `4` | `remote_exec` 默认并发数;模型可请求更小值,不能超过此配置 |
|
|
131
|
+
| `maxBatchOutputBytes` | `262144` | 一次 `remote_exec` 所有任务共享的 stdout/stderr 总预算 |
|
|
132
|
+
| `remoteFileReadBytes` | `262144` | `remote_read` 单次读取上限(字节) |
|
|
133
|
+
| `remoteFileEditBytes` | `2097152` | `remote_write` / `remote_edit` 的文件上限(字节) |
|
|
134
|
+
| `remoteFileWrite` | `false` | 是否启用 `remote_write` / `remote_edit`;默认关闭 |
|
|
135
|
+
| `remoteGlobMaxResults` | `200` | `remote_glob` 结果上限 |
|
|
136
|
+
| `remoteGlobMaxDepth` | `4` | `remote_glob` 递归深度上限 |
|
|
137
|
+
| `remoteGrepMaxResults` | `200` | `remote_grep` 结果上限 |
|
|
138
|
+
| `transferChunkTimeoutMs` | `30000` | SFTP 分块读写看门狗;卡死时关闭会话,UI 可从 offset 续传 |
|
|
139
|
+
| `connectTimeoutMs` | `15000` | SSH 建连超时 |
|
|
140
|
+
| `hostKeyMode` | `strict` | `strict` / `accept-new` |
|
|
141
|
+
| `connectionPool` | `true` | 每台服务器复用一条连接 |
|
|
142
|
+
| `poolIdleMs` | `90000` | 空闲连接回收时间 |
|
|
143
|
+
| `poolMaxServers` | `8` | 连接池最多缓存的目标数 |
|
|
144
|
+
| `maxChannelsPerServer` | `6` | 单连接并发通道上限 |
|
|
145
|
+
| `prewarm` | `true` | UI 选中服务器时预热连接 |
|
|
146
|
+
| `targetContext` | `true` | 每轮注入纳管资源清单与 `@资源名` 提示;关掉后不注入上下文(工具仍可显式调用) |
|
|
147
|
+
| `localRoot` | `""`(空 = 用户主目录) | 工作台本地文件面板与本地终端的根目录;必须是绝对路径或 `~/...`,非法值回退用户主目录 |
|
|
148
|
+
|
|
149
|
+
> 右侧栏的工作台开关不在 `cordis.patch.yml` 里,而是在 **插件管理 → 远程工作台** 页面里的「在侧边栏显示远程工作台」;它只影响 UI 入口,不影响模型工具。
|
|
150
|
+
|
|
151
|
+
示例:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
- id: dsh-ssh-workbench
|
|
155
|
+
name: 'dsh-ssh-workbench'
|
|
156
|
+
config:
|
|
157
|
+
commandTimeoutMs: 300000
|
|
158
|
+
maxOutputBytes: 262144
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## 开发
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm test # 单元测试(无需网络或真实 SSH)
|
|
165
|
+
npm run test:ssh # SSH 传输集成(进程内 ssh2.Server)
|
|
166
|
+
npm run test:ssh:sftp # SFTP 集成(进程内 ssh2.Server)
|
|
167
|
+
npm run pack # 打包到 dist/
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
更多文档:[快速上手](docs/quickstart.md) · [执行模型](docs/execution-model.md) · [架构说明](docs/architecture.md) · [贡献指南](CONTRIBUTING.md) · [安全说明](SECURITY.md) · [更新日志](CHANGELOG.md)。
|
|
171
|
+
|
|
172
|
+
## 参与贡献
|
|
173
|
+
|
|
174
|
+
- 开发与提交约定见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
175
|
+
- 安全问题请按 [SECURITY.md](SECURITY.md) 私下报告,不要开公开 issue。
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT。终端渲染使用随包分发的 [xterm.js](https://github.com/xtermjs/xterm.js)(MIT),许可文本见 [vendor/XTERM-LICENSE](vendor/XTERM-LICENSE)。
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SSH Workbench(dsh-ssh-workbench)安全说明
|
|
2
|
+
|
|
3
|
+
## 报告漏洞
|
|
4
|
+
|
|
5
|
+
请通过 GitHub 的 **Security → Report a vulnerability** 私下报告(或直接联系维护者),不要在公开 issue 中披露未修复的安全问题。
|
|
6
|
+
|
|
7
|
+
## 设计说明
|
|
8
|
+
|
|
9
|
+
- **凭据**:使用 AES-256-GCM 加密存储在 `$DSH_HOME/ssh-workbench/secrets.json`,密钥在 `secrets.key`(0600);纳管资源清单不存明文密码。
|
|
10
|
+
- **主机指纹**:插件维护自己的 `known_hosts`,默认 `strict`(未知指纹拒绝);UI 显示 SHA256 指纹并询问是否信任,由用户决定;信任后保存。指纹变化时连接会被拒绝并提示「服务器主机密钥已变化」,用户核对后可显式「更新信任」。
|
|
11
|
+
- **审批**:插件不是权限边界,只硬阻断机器级黑名单;其余远程命令是否弹审批完全由 DSH 会话预设决定(ask / never)。当前不提供命令分级、AI 风险解释或审计。
|
|
12
|
+
- **批量执行**:`remote_exec` 对每个 task 按其目标方言单独跑黑名单,任一 task 命中即整批拒绝;批量工具不依赖任何会话目标状态,但每个 task 都必须是已纳管的远程资源(`server: "local"` 被拒绝)。它仅前台执行,并受 `maxBatchTasks` / `batchConcurrency` / `maxBatchOutputBytes` 限制;这些限制不是安全边界。
|
|
13
|
+
- **黑名单是 best-effort**:`;`、`&&`、`|`、单个 `&`、换行都会逐段检查;每段会先按 shell 语义还原(去掉引号/反斜杠、剥离 `sudo`/`timeout`/`env`/`!`/赋值等包装、路径词法规范化/glob 前缀、`$()`/反引号/`eval` 递归),所以 `"mkfs.ext4"`、`\rm -rf /`、`rm -rf /etc/`、`cat > "/dev/sda"`、`echo "$(mkfs...)"` 都会命中。但 `bash -c`、base64、脚本文件、解释器 heredoc 正文不会被解析,真正的安全边界仍是 SSH 最小权限 + host-key 固定 + 网络隔离 + DSH 审批。
|
|
14
|
+
- **远程文件工具**:`remote_read`/`remote_glob`/`remote_grep` 只是把显式 `server` 的 SFTP/远程搜索暴露给模型;`remote_write`/`remote_edit` 默认关闭,开启后仍以 SSH 账号权限直接写目标文件,不做原子替换、不做备份。读写路径必须绝对路径或 `~`,但仍然没有远端目录白名单;真正的边界是 SSH 最小权限、host-key 固定和 DSH 审批。
|
|
15
|
+
- **本地↔远端传输**(右侧栏文件面板的 SFTP 上传/下载)以 DSH 宿主进程的 OS 权限读写本地文件;请把宿主账号视为可信。
|
|
16
|
+
- **用户终端**:远程工作台的终端是用户主动输入,不经过 agent 的审批、沙箱或黑名单;远程终端使用同一份 SSH 凭据与主机校验,本地终端以 Host 进程权限运行。终端会话随页签/页面关闭而结束,并受空闲回收约束。
|
|
17
|
+
- **本地文件管理**:工作台的本地文件浏览默认限制在宿主用户主目录内(插件配置 `localRoot` 可改为其他绝对路径),解析真实路径后拒绝越界与符号链接逃逸;它不是模型工具,也不暴露给 agent。
|
|
18
|
+
- **远程 shell 绕过本地沙箱**:远程命令在服务器上执行,不受 DSH 本地文件沙箱约束(macOS/Linux 宿主的 POSIX 目标经原生 `bash` 路由,Windows 宿主的 POSIX 目标经 `remote_bash`,Windows 目标在任一宿主上都经 `remote_pwsh`);请用最小权限 SSH 账号并保持 host-key `strict`。
|
|
19
|
+
- **依赖**:发布包没有安装期运行时依赖;SSH 传输使用随包携带的纯 JS `ssh2` 1.17.0 及其依赖(版本与许可证见 `vendor/NOTICE.md`)。这样做避免了插件安装时执行第三方原生构建脚本,但意味着 ssh2 的安全更新需要随插件版本一起发布;维护者应定期跟进上游安全公告并升级 vendor 副本。
|
|
20
|
+
- **Windows 目标黑名单**:`remote_pwsh` 使用单独的 PowerShell 规则集(`Format-Volume`、`Clear-Disk`、`diskpart`、`bcdedit`、`cipher /w`、卷影删除、整盘递归删除等);同样只是防误操作的 best-effort,不是权限边界。
|
|
21
|
+
|
|
22
|
+
## 运维建议
|
|
23
|
+
|
|
24
|
+
- 给插件使用独立的最小权限 SSH 账号;host-key 保持 `strict`。
|
|
25
|
+
- 生产服务器优先使用 `ask` / 自动审查预设;`完全权限` 只在受控环境使用。
|
|
26
|
+
- 重要变更前先备份;黑名单不能替代备份。
|
|
27
|
+
- 不要提交 `servers.json`、`secrets.json`、`secrets.key`、`known_hosts` 或活动日志。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# SSH Workbench(dsh-ssh-workbench)架构(v1.7.0)
|
|
2
|
+
|
|
3
|
+
> 产品定位与用法见 [README.md](../README.md);本文只讲实现分层。
|
|
4
|
+
|
|
5
|
+
## 分层
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
lib/
|
|
9
|
+
├── index.js 插件入口:Config、apply、工具注册(remote_exec + remote_bash + remote_pwsh + remote 文件工具)
|
|
10
|
+
├── routes.js UI 数据面:资源管理 / 连接测试 / SFTP 文件面板 / 侧边栏偏好
|
|
11
|
+
├── client.js Client 半片:左侧导航远程工作台、设置页、@ 引用 chip、右侧栏文件面板
|
|
12
|
+
└── core/
|
|
13
|
+
├── mentions.js 每会话 @ 提示:mention 解析、去重、历史恢复(不绑定执行目标)
|
|
14
|
+
├── managed-context.js agent/pre-step:每轮注入资源清单与 @ 提示上下文
|
|
15
|
+
├── posix-shell.js POSIX 远程 shell 辅助:workdir、超时包装
|
|
16
|
+
├── remote-shell-tool.js 显式 server 的 remote_bash / remote_pwsh 工具
|
|
17
|
+
├── remote-exec-tool.js 插件自有 remote_exec:显式目标、一轮 fan-out、并发池、逐任务 guard/超时/输出
|
|
18
|
+
├── remote-file-tools.js 远程文件工具:remote_read/write/edit/glob/grep(SFTP + 方言 grep;写开关)
|
|
19
|
+
├── execution-stats.js 按 Session 聚合 activity 中的远程执行次数(composer dock)
|
|
20
|
+
├── live-executions.js 进行中的远程调用(内存 in-flight,供 composer pill 实时显示,不参与执行)
|
|
21
|
+
├── server-ref.js 资源名/ID 解析与 requireServerRef 错误
|
|
22
|
+
├── shell-adapters.js 方言适配器入口:POSIX 适配器、按方言选择、宿主平台判断
|
|
23
|
+
├── win32-adapter.js Windows 适配器:PowerShell -EncodedCommand、UTF-8、退出码、workdir
|
|
24
|
+
├── dialect.js 目标方言:facts.dialect 优先、platform 仅作提示
|
|
25
|
+
├── guard.js ctx.tools.guard:危险命令黑名单(只 deny,按工具路由规则集)
|
|
26
|
+
├── gate.js POSIX 命令黑名单规则与分类
|
|
27
|
+
├── gate-pwsh.js PowerShell 命令黑名单规则与分类
|
|
28
|
+
├── background.js 后台任务:POSIX nohup / Windows Start-Process 启动器、pid/日志标记解析与展示
|
|
29
|
+
├── posix-quote.js POSIX 单引号转义(路由与后台启动器共用)
|
|
30
|
+
├── store.js 服务器清单与持久化
|
|
31
|
+
├── secrets.js 密码/私钥口令(0600,本机密钥加密)
|
|
32
|
+
├── known-hosts.js 主机身份校验与信任记录
|
|
33
|
+
├── broker.js 连接池、通道池、超时/取消/输出截断
|
|
34
|
+
├── ssh.js runtime 门面:exec / test / warm / SFTP
|
|
35
|
+
├── errors.js 错误分类与建议
|
|
36
|
+
├── activity.js UI 概览用本地操作记录(非审计)
|
|
37
|
+
├── prefs.js 侧边栏等 UI 偏好(~/.dsh/ssh-workbench/prefs.json,0600)
|
|
38
|
+
├── terminals.js 交互终端会话:远程 SSH shell + 本地 PTY,缓冲/轮询/空闲回收
|
|
39
|
+
├── local-file-api.js 本地文件 API:以合成 __local__ 资源复用同一条 /sftp/* 路由面
|
|
40
|
+
├── localfs.js 本地根目录(默认用户主目录,可用 localRoot 配置)内的文件浏览与写入(realpath 规范化;越界/符号链接写入与删除防穿透)
|
|
41
|
+
└── paths.js 数据目录与私有文件写入
|
|
42
|
+
|
|
43
|
+
vendor/
|
|
44
|
+
├── xterm.js 终端渲染构建输入(xterm.js UMD,MIT;实际已内联进 lib/client.js,无运行时路由)
|
|
45
|
+
├── ssh2/ 纯 JS SSH 传输实现(MIT,随包携带;broker.js 直接 import)
|
|
46
|
+
└── node_modules/ ssh2 的纯 JS 运行时依赖(asn1 / bcrypt-pbkdf / safer-buffer / tweetnacl)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`ssh2` 不通过 `dependencies` 安装,而是以纯 JS 形式 vendor 进发布包(`vendor/ssh2` + `vendor/node_modules/`);broker 用相对路径动态 import。这样插件安装不触发 `node-gyp`、不需要本机编译工具链、也不依赖 npm 网络,代价是包体积略大。ssh2 的可选原生模块(`cpu-features`、`nan`、`sshcrypto.node`)全部未携带,ssh2 会回退到纯 JS 实现。
|
|
50
|
+
|
|
51
|
+
## 一次远程工具执行的完整路径
|
|
52
|
+
|
|
53
|
+
1. 用户消息进入 `agent/pre-step`。
|
|
54
|
+
2. `managed-context.js`:
|
|
55
|
+
- 从宿主消息里提取 `@名字`,匹配服务器清单;
|
|
56
|
+
- 仅记录为 @ 提示(candidates),不产生 active target;每轮注入资源清单与显式 server 工具规则,模型自行选择目标;
|
|
57
|
+
- 注入一条 user-role 资源上下文(source kind `ssh-workbench-target`),并 spread 下游 decision。
|
|
58
|
+
3. 模型选择一个远程工具并显式传 `server`:`remote_bash` / `remote_pwsh` / `remote_exec`,或远程文件工具 `remote_read` / `remote_glob` / `remote_grep`(写入另需 `remoteFileWrite`)。原生 `bash` / `pwsh` 不经过插件,始终本地执行。
|
|
59
|
+
4. DSH 审批(本插件不参与)。
|
|
60
|
+
5. `ctx.tools.guard()`(`guard.js`):从 `arguments.server`(或 `remote_exec.tasks[]` 的每个 server)解析目标,按对应方言检查黑名单 → 命中直接拒绝。`gate.js` 先把每段还原成 argv words(去引号/转义、剥 `sudo`/`timeout` 等包装、路径规范化、重定向按 token、`$()`/`eval` 递归)再匹配。
|
|
61
|
+
6. 工具实现(`remote-shell-tool.js` / `remote-exec-tool.js` / `remote-file-tools.js`):
|
|
62
|
+
- `remote_bash` / `remote_pwsh` 处理 `workdir`(绝对路径 / `~` / 相对路径 → `$HOME` 前缀);
|
|
63
|
+
- 逐 server 解析方言与 `timeout` 能力(`kill`/`basic`/`none` + 登录 shell 路径;`remote_exec` 的 `probe:false` 跳过探测);
|
|
64
|
+
- 可用时把命令包成 `timeout [-k N] <秒> '<shell>' -c '<命令>'`,让客户端超时同时终止远端进程组;
|
|
65
|
+
- `runtime.exec(server, command, { timeoutMs, signal, cap, stderrCap, connectTimeoutMs })`;broker 建通道后立即关闭 stdin(等价 `</dev/null`),把远端 `timeout` 的 exit 124 映射回 timeout 结果,并对 stdout/stderr 分别限长;
|
|
66
|
+
- 返回带 server / dialect / cwd / 退出码 / stdout / stderr 的文本结果;超时/取消说明远端是否已终止。
|
|
67
|
+
7. `remote_exec` 的多个 task 在同一个 assistant step 内并发执行(最多 `batchConcurrency`),结果按输入顺序渲染;`isConcurrencySafe` 让 DSH 的并行调度器可以真正重叠调用。
|
|
68
|
+
|
|
69
|
+
## 多资源并行
|
|
70
|
+
|
|
71
|
+
`remote_exec({ tasks: [{ server, command, workdir?, timeoutMs? }], concurrency? })` 是插件自有的 `defineTool`,不走 `tools/execute`:
|
|
72
|
+
|
|
73
|
+
- **显式目标**:每个 task 自己带 `server`,不依赖也不修改任何会话目标状态;`server: "local"` 被拒绝,任务只能落在纳管资源上。
|
|
74
|
+
- **全量预检**:先解析全部 server、方言、workdir、timeout;任一 task 非法则整批拒绝,不会执行一半。
|
|
75
|
+
- **真正并发**:工具声明 `isConcurrencySafe: () => true`,DSH 的 batch 调度会把它和其他 parallel-safe 调用一起放进有界并发池;工具内部再按 `batchConcurrency`(默认 4,最大 8)跨服务器并发。单台服务器仍受 `maxChannelsPerServer` 约束。
|
|
76
|
+
- **逐任务隔离**:每个 task 按自己的 `facts.dialect` 选择 POSIX/Windows 适配器与 guard 规则;超时、传输失败、非零退出码只影响该任务,结果按输入顺序渲染。
|
|
77
|
+
- **输出预算**:一次 batch 共享 `maxBatchOutputBytes`(默认 256 KiB),每个 task 先取 `maxOutputBytes` 与均分预算的较小值,避免一次 fan-out 撑爆上下文。
|
|
78
|
+
- **仅前台**:不支持 `run_in_background`;长任务按目标分别用后台工具或分别在终端里跑。
|
|
79
|
+
- **UI**:composer 下方 pill 是执行账本的只读投影,结构对齐 DSH 内置 Token 用量弹窗:标题行「远程执行」+ 本 Session 总次数、0.5px 分隔线、目标名 → 执行次数。不显示成功/失败、用时或模型指标,也不承担切换或打开工作台;终端/文件从左侧工作台进入。弹层通过 `react-dom.createPortal` 挂到 `document.body`,避免被 composer 的 stacking context 裁切。没有执行记录时不显示,因此不存在“切本地后残留”。
|
|
80
|
+
|
|
81
|
+
## 多宿主与多方言
|
|
82
|
+
|
|
83
|
+
DSH 的内置 shell 工具按宿主平台二选一:POSIX 宿主有 `bash`、没有 `pwsh`;Windows 宿主有 `pwsh`、没有 `bash`。远程执行不依赖宿主自带的 shell——三种远程工具在**两种宿主上都注册**,按目标方言分派:
|
|
84
|
+
|
|
85
|
+
| 目标方言 | 工具 | 执行器 |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| POSIX | `remote_bash` | `posixAdapter` + 可选远端 `timeout` 包装 |
|
|
88
|
+
| Windows | `remote_pwsh` | `win32Adapter`(UTF-16LE `-EncodedCommand`) |
|
|
89
|
+
| 混合(多目标) | `remote_exec` | 逐 task 按各自方言选适配器 |
|
|
90
|
+
|
|
91
|
+
- 目标方言由连接后的探测决定(`uname -s` 成功 → posix;否则 `cmd /c ver` 成功 → windows),写入 `facts.dialect`;`platform` 字段只作探测前提示。`effectiveDialect()` 是唯一读取入口。
|
|
92
|
+
- 方言决定命令构造:POSIX 用 `cd -- ... && ...` + 可选 `timeout` 包装;Windows 用 UTF-16LE base64 的 `powershell -EncodedCommand` 脚本(内含 UTF-8 输出、`Set-Location`、显式退出码),没有远端 `timeout`。
|
|
93
|
+
- `remote_bash` 与 `remote_pwsh` 互相拒绝错误方言并给出改用建议;即使 `platform` 手填错误,探测结果也会纠正。
|
|
94
|
+
- `ctx.tools.guard()` 按每个显式 server 的方言选择 POSIX 或 PowerShell 规则集;facts 缺失时两套都跑。
|
|
95
|
+
|
|
96
|
+
## 后台任务
|
|
97
|
+
|
|
98
|
+
`run_in_background: true` 不再报错,而是启动一个**脱离 SSH 会话**的远端进程:
|
|
99
|
+
|
|
100
|
+
- POSIX:`sh -c '<脚本>'` 内用 `nohup "$SHELL" -lc` 启动命令,stdout/stderr 重定向到 `mktemp /tmp/dsh-ssh-workbench-bg.XXXXXX` 生成的日志;启动器打印 `__SDH_BG_PID__` / `__SDH_BG_LOG__` 标记。
|
|
101
|
+
- Windows:外层 PowerShell 用 `Start-Process -RedirectStandardOutput/-RedirectStandardError -WindowStyle Hidden -PassThru` 启动内层编码脚本,输出 `__SDH_BG_PID__` / `__SDH_BG_LOG__` / `__SDH_BG_ERR__`。
|
|
102
|
+
- 工具/路由解析标记后返回 pid、日志路径与读取/停止命令;启动失败(没有 pid)返回 `REMOTE_BACKGROUND_FAILED`。
|
|
103
|
+
- 远端进程不在 DSH 本地 `job_*` 注册表里:`job_output` 看不到它,模型用 `tail` / `Get-Content` 取输出、`kill` / `Stop-Process` 停止;`timeoutMs` 与取消只作用于启动调用。
|
|
104
|
+
|
|
105
|
+
## 关键契约(来自 DSH,非本插件发明)
|
|
106
|
+
|
|
107
|
+
- `tools/execute`:`(exec, next) => ToolExecutionResult`,返回 `{ value }` 替换执行;官方插件 `dsh-tool-call-timeout-policy` 同款用法。
|
|
108
|
+
- `agent/pre-step`:返回 `{ kind: 'enter', messages }`,改写必须 spread。
|
|
109
|
+
- `ctx.tools.guard()`:同步、单调的拒绝;在审批之后执行。
|
|
110
|
+
- 原生 `bash` 输出值:`{ kind:'foreground', cwd, exitCode, signal, timedOut, aborted, timeoutMs, stdout, stderr }`。
|
|
111
|
+
- 模型可见工具:`remote_exec`、`remote_bash`、`remote_pwsh`、`remote_read`、`remote_glob`、`remote_grep`、`remote_write`、`remote_edit` 在两种宿主上都注册;默认只读工具可用,写工具在 `remoteFileWrite=true` 前明确拒绝。原生 `bash` / `pwsh` / `read` / `write` / `edit` / `glob` / `grep` 保持本地语义。`remote_exec` 与只读文件工具声明 `isConcurrencySafe:true`,shell 与写文件工具保持 exclusive。
|
|
112
|
+
|
|
113
|
+
## @ 提示生命周期
|
|
114
|
+
|
|
115
|
+
- 存储:进程内 `Map<sessionId, {candidates, version, injectedVersion, seeded}>`;只有 @ 提示,没有执行绑定。
|
|
116
|
+
- 来源:人类消息里的 `@名字`,由 `mentions.js` 解析并匹配清单;`managed-context.js` 每轮把它连同资源清单写进上下文。
|
|
117
|
+
- 恢复:会话恢复后首次 `agent/pre-step` 用 `session.deriveMessages()` 回放历史人类消息。
|
|
118
|
+
- 清理:`agent/disposed` 或插件卸载。
|
|
119
|
+
|
|
120
|
+
## 执行账本
|
|
121
|
+
|
|
122
|
+
- `activity.js` 记录每次远程调用(`server` / `sessionId` / `durationMs` / `outcome`),是 composer pill 的唯一数据源。
|
|
123
|
+
- `execution-stats.js` 按 Session 聚合出 `{count, servers:[{name,count}]}`;`live-executions.js` 只保存进行中的调用,两者都不参与执行决策,也不写入 Session。
|
|
124
|
+
|
|
125
|
+
## 降级行为
|
|
126
|
+
|
|
127
|
+
| 情况 | 行为 |
|
|
128
|
+
|---|---|
|
|
129
|
+
| 没有 @ 提示 | 原生工具完全本地,零开销 |
|
|
130
|
+
| `targetContext:false` | 不注入资源清单/`@` 提示;显式 server 工具仍可用 |
|
|
131
|
+
| `ctx.tools.guard` 不可用(旧 DSH) | 记录 warning,黑名单关闭,其余功能不变 |
|
|
132
|
+
| 目标被删除/禁用/方言不匹配 | 返回结构化错误,提示重新选择目标或改用另一种 remote 工具 |
|
|
133
|
+
| 远端没有 `timeout` 工具 | 命令不加包装;超时只停止等待,stderr 提示远端可能仍在运行 |
|
|
134
|
+
| 非 ASCII 服务器名 | host/client 两侧都支持 Unicode 名称;`@测试机` 可直接绑定 |
|
|
135
|
+
| host facts 采集失败 | 只影响 UI/上下文摘要,不影响执行;下次测试/选中目标时重试 |
|
|
136
|
+
| SFTP 分块卡死 | `transferChunkTimeoutMs` 到点关闭会话并报 `SFTP_TIMEOUT`,UI 可从 offset 续传 |
|
|
137
|
+
| 远程 home 解析失败 | `/sftp/home` 返回可读错误,工作台文件面板回退到 `/` |
|
|
138
|
+
| 多个 `@` | 模型按资源清单选择目标,或直接发 `remote_exec` 并行;没有可切换的 active target,不会静默跑本地 |
|
|
139
|
+
| 服务器清单损坏 | 路由返回错误结果,不影响本地工具 |
|
|
140
|
+
|
|
141
|
+
## 安全边界
|
|
142
|
+
|
|
143
|
+
- 黑名单只拦机器毁灭类命令,不是权限边界。`remote_exec` 的每个 task 都会按目标方言过 guard,但批量并发不改变权限边界。
|
|
144
|
+
- 远程命令不受 DSH 本地沙箱约束;是否审批由 DSH 会话预设决定。
|
|
145
|
+
- 凭据与主机身份:见 `secrets.js` / `known-hosts.js`;未知指纹不会被静默信任。
|
|
146
|
+
- 插件不写 Session 事件、不做审计日志;`activity.json` 只是 UI 概览。
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# 执行模型与设计取舍
|
|
2
|
+
|
|
3
|
+
本文说明「远程执行如何确定目标」这件事的设计结论,以及为此放弃的方案和原因。实现分层见 [architecture.md](architecture.md);用户用法见 [quickstart.md](quickstart.md)。
|
|
4
|
+
|
|
5
|
+
## 一句话结论
|
|
6
|
+
|
|
7
|
+
**每个远程工具调用自己带 `server`。** 不存在会话级「当前目标」,也不存在把原生工具透明改道到远端。
|
|
8
|
+
|
|
9
|
+
## 为什么不要「当前目标」
|
|
10
|
+
|
|
11
|
+
早期版本用一个会话级 active target 表示「现在操作哪台机器」,`@资源名` 会改写它。它在三个地方同时承担职责:
|
|
12
|
+
|
|
13
|
+
1. **执行绑定** —— 原生 `bash` 被路由到 active target;
|
|
14
|
+
2. **上下文状态** —— 每轮把 active target 写进提示;
|
|
15
|
+
3. **UI 状态** —— composer 下方显示当前目标。
|
|
16
|
+
|
|
17
|
+
三者耦合带来一组必然问题:
|
|
18
|
+
|
|
19
|
+
- 多目标任务只能逐个 `switch → 执行 → switch`,模型把时间花在切换上,人也看不出「这台到底跑了什么」;
|
|
20
|
+
- 多个 `@` 时 active 被清空,模型既不知道该跑哪台,也拿不到资源清单;
|
|
21
|
+
- 切回本地是状态变更,容易残留成「以为在本机、其实在远端」;
|
|
22
|
+
- 目标会漂移,事后无法从对话里复现「这条命令跑在哪台」。
|
|
23
|
+
|
|
24
|
+
所以把它整体去掉。现在:
|
|
25
|
+
|
|
26
|
+
| 场景 | 做法 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| 单个目标 | `remote_bash({ server, command })` |
|
|
29
|
+
| 多个目标并行 | `remote_exec({ tasks: [{ server, command }] })` |
|
|
30
|
+
| 本机操作 | 原生 `bash` / `pwsh` / `read` / `write` / … |
|
|
31
|
+
| 远程文件 | `remote_read` / `remote_glob` / `remote_grep`(写需 `remoteFileWrite`) |
|
|
32
|
+
|
|
33
|
+
**每个调用的目标都写在参数里**,因此并行安全、可审计、可复现,也不存在「状态残留」。执行工具在两种宿主上都注册,避免宿主自带 shell 平台差异(Windows 宿主没有原生 `bash`)影响可用性。
|
|
34
|
+
|
|
35
|
+
## `@资源名` 是什么
|
|
36
|
+
|
|
37
|
+
**提示,不是绑定。** 用户 `@web-1` 时:
|
|
38
|
+
|
|
39
|
+
- `mentions.js` 解析出候选名字,只作为本会话的提示存储;
|
|
40
|
+
- `managed-context.js` 每轮把它连同资源清单写进上下文;
|
|
41
|
+
- 模型据此在工具参数里填 `server`。
|
|
42
|
+
|
|
43
|
+
`@` 不改变任何执行状态,所以后续无 `@` 的追问不会把命令静默带到别的机器;用户仍可随时 `@` 另一台或直接说「在本机看」。
|
|
44
|
+
|
|
45
|
+
## 上下文注入
|
|
46
|
+
|
|
47
|
+
每轮都注入一份紧凑资源清单(名称 / 方言 / 状态),而不是只在「有 active target」或「多个候选」时才注入。理由是:没有任何提示时,模型只能去读 `~/.dsh/ssh-workbench/servers.json` 猜配置——既慢,又把凭据目录暴露给模型。上下文里同时明确:
|
|
48
|
+
|
|
49
|
+
- 远程操作只能用 `remote_*` 工具,且必须显式写 `server`;
|
|
50
|
+
- 原生 shell / 文件工具只在本机生效;
|
|
51
|
+
- 目标不明确时按清单选择,或在有歧义时先问。
|
|
52
|
+
|
|
53
|
+
## 执行账本与 pill
|
|
54
|
+
|
|
55
|
+
composer 下方的 pill 是**只读投影**,不是控制器:
|
|
56
|
+
|
|
57
|
+
- 数据来自 `activity.js`(每次远程调用的 `server` / `sessionId` / `durationMs` / `outcome`),由 `execution-stats.js` 按 Session 聚合;
|
|
58
|
+
- 进行中的调用由 `live-executions.js` 在内存里记录,仅用于显示「正在连哪台」,不参与执行;
|
|
59
|
+
- 视觉对齐 DSH 内置的 Token 用量弹窗:标题 + 总次数、分隔线、目标名 → 次数;
|
|
60
|
+
- 不显示成功/失败、用时或模型指标(这些是 DSH 会话的私有数据,插件不伪造),不承担切换目标或打开工作台。
|
|
61
|
+
|
|
62
|
+
## 状态类批量查询的快路径
|
|
63
|
+
|
|
64
|
+
「看看几台机器状态」这类请求最大的延迟来源不是 SSH 跑命令,而是**对不可达目标的探测**:方言探测(每次最多一个 `connectTimeoutMs`)+ `timeoutGuard` 能力探测(再一次)+ 真正建连(再一次)。
|
|
65
|
+
|
|
66
|
+
因此 `remote_exec` 提供两个参数:
|
|
67
|
+
|
|
68
|
+
- `probe:false` —— 跳过方言与 `timeoutGuard` 探测,直接用 `facts.dialect` / 记录平台判断;
|
|
69
|
+
- `connectTimeoutMs` —— 按调用限制建连时间,一路透传到 broker 的 `readyTimeout`。
|
|
70
|
+
|
|
71
|
+
组合后 6 台目标里 3 台不可达时,整体从接近一分钟降到秒级;不可达任务只让自己失败,不阻塞其他任务。
|
|
72
|
+
|
|
73
|
+
## 有意的非目标
|
|
74
|
+
|
|
75
|
+
| 不做 | 原因 |
|
|
76
|
+
|---|---|
|
|
77
|
+
| 把 `read` / `write` / `edit` 透明改道到远端 | 会让「改文件」的语义取决于隐式状态,比多做几个工具危险得多;需要远程文件就用显式的 `remote_*` |
|
|
78
|
+
| SSHFS / 远程 IDE 式挂载 | 需要额外守护进程与权限模型,和「零安装依赖」的发布约束冲突 |
|
|
79
|
+
| 命令分级(allow/guarded)与风险解释 | 白名单可被 shell 语法绕过,容易制造错误的安全感;真正的边界是 SSH 权限、host-key 与 DSH 审批 |
|
|
80
|
+
| 插件自有审批清单 | 审批策略属于 DSH 会话预设,插件重复实现会互相矛盾 |
|
|
81
|
+
| 伪造模型指标(TTFT / TPS / 轮次) | 这些数据只存在于 DSH 宿主,插件无法准确获得 |
|
|
82
|
+
|
|
83
|
+
黑名单 guard 保留为兜底:同步、单调、按每个显式目标的方言逐条检查(`remote_exec` 的每个 task 都过),但它**不是权限边界**——见 [SECURITY.md](../SECURITY.md)。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|