@tiphareth/dsh-hardssh 0.1.2 → 0.2.3
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/LICENSE +30 -0
- package/README.md +77 -50
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1171 -1321
- package/lib/fs.js +333 -416
- package/lib/index.js +5428 -2991
- package/lib/ledger-B-LXlftp.js +43 -0
- package/lib/ledger-D2ezq1iW.js +376 -0
- package/lib/model-Cp7f70Mb.js +5 -0
- package/lib/registry-CViuzKYI.js +421 -0
- package/lib/subprocess.js +92 -549
- package/lib/types/backend.d.ts +73 -78
- package/lib/types/base/capability.d.ts +4 -0
- package/lib/types/base/ledger-router.d.ts +42 -26
- package/lib/types/base/ledger.d.ts +30 -19
- package/lib/types/base/model.d.ts +17 -8
- package/lib/types/base/plugin.d.ts +11 -0
- package/lib/types/base/registry.d.ts +28 -31
- package/lib/types/base/router.d.ts +0 -7
- package/lib/types/client/api.d.ts +8 -8
- package/lib/types/client/connect-host.d.ts +13 -0
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/locales.d.ts +15 -1
- package/lib/types/client/session-connect-gate.d.ts +71 -0
- package/lib/types/client/ssh/api.d.ts +11 -9
- package/lib/types/client/ssh/apply.d.ts +14 -8
- package/lib/types/client/ssh/locales.d.ts +8 -1
- package/lib/types/client/ssh/ops-tab.d.ts +34 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
- package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
- package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
- package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
- package/lib/types/client/ssh/session-target.d.ts +17 -0
- package/lib/types/client/state.d.ts +4 -1
- package/lib/types/client/workspace-panel-entry.d.ts +30 -0
- package/lib/types/client/workspace-panel.d.ts +24 -0
- package/lib/types/client-http.d.ts +77 -6
- package/lib/types/core.d.ts +5 -16
- package/lib/types/fs.d.ts +42 -25
- package/lib/types/index.d.ts +138 -9
- package/lib/types/ledger.d.ts +16 -111
- package/lib/types/protocol.d.ts +0 -5
- package/lib/types/providers/index.d.ts +6 -3
- package/lib/types/providers/local/provider.d.ts +83 -23
- package/lib/types/providers/ssh/provider.d.ts +24 -57
- package/lib/types/remote/environment.d.ts +2 -0
- package/lib/types/remote/remote-fs.d.ts +12 -1
- package/lib/types/remote/remote-process.d.ts +22 -1
- package/lib/types/remote/remote-subprocess.d.ts +13 -1
- package/lib/types/remote/remote-terminal.d.ts +4 -0
- package/lib/types/remote-search.d.ts +3 -3
- package/lib/types/routes.d.ts +36 -8
- package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
- package/lib/types/runtime/workspace-core.d.ts +73 -38
- package/lib/types/runtime/workspace-migration.d.ts +113 -0
- package/lib/types/ssh/connection/lease.d.ts +23 -0
- package/lib/types/ssh/connection/manager.d.ts +359 -0
- package/lib/types/ssh/connection/pool.d.ts +8 -1
- package/lib/types/ssh/engine.d.ts +97 -267
- package/lib/types/ssh/known-hosts.d.ts +22 -5
- package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
- package/lib/types/ssh/plugin.d.ts +10 -15
- package/lib/types/ssh/protocol.d.ts +18 -0
- package/lib/types/ssh/routes.d.ts +15 -7
- package/lib/types/ssh/sftp/service.d.ts +216 -0
- package/lib/types/ssh/store.d.ts +20 -8
- package/lib/types/ssh/terminal/service.d.ts +49 -0
- package/lib/types/ssh/tunnel/service.d.ts +26 -0
- package/lib/types/ssh/vault.d.ts +51 -4
- package/lib/types/subprocess.d.ts +13 -21
- package/lib/types/switch/switch-fs.d.ts +52 -2
- package/lib/types/switch/switch-subprocess.d.ts +32 -3
- package/lib/types/tools.d.ts +7 -6
- package/lib/types/workspace-tool-ops.d.ts +33 -0
- package/lib/types/workspace.d.ts +18 -0
- package/lib/vault-3gpWct2Q.js +559 -0
- package/lib/workspace.js +2 -0
- package/package.json +59 -26
- package/scripts/export-legacy-workspaces.mjs +136 -0
- package/src/backend.ts +155 -483
- package/src/base/capability.ts +4 -0
- package/src/base/ledger-router.ts +214 -65
- package/src/base/ledger.ts +175 -48
- package/src/base/model.ts +18 -8
- package/src/base/plugin.ts +15 -4
- package/src/base/registry.ts +49 -41
- package/src/base/router.ts +0 -8
- package/src/client/api.ts +8 -36
- package/src/client/connect-host.ts +199 -0
- package/src/client/directory-flow.tsx +91 -36
- package/src/client/index.ts +56 -53
- package/src/client/locales.ts +14 -0
- package/src/client/session-connect-gate.ts +163 -0
- package/src/client/ssh/api.ts +245 -108
- package/src/client/ssh/apply.ts +23 -24
- package/src/client/ssh/locales.ts +16 -2
- package/src/client/ssh/ops-tab.tsx +79 -0
- package/src/client/ssh/panel/ClusterTab.tsx +9 -20
- package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
- package/src/client/ssh/panel/SshPanel.tsx +81 -77
- package/src/client/ssh/panel/TerminalTab.tsx +18 -37
- package/src/client/ssh/panel/TransferTab.tsx +43 -33
- package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
- package/src/client/ssh/panel/panel.module.css +90 -127
- package/src/client/ssh/session-target.ts +69 -0
- package/src/client/state.ts +9 -1
- package/src/client/workspace-badges.ts +99 -96
- package/src/client/workspace-panel-entry.tsx +91 -0
- package/src/client/workspace-panel.tsx +227 -0
- package/src/client/workspace.module.css +76 -2
- package/src/client-http.ts +118 -18
- package/src/core.ts +37 -47
- package/src/fs.ts +171 -85
- package/src/index.ts +535 -244
- package/src/ledger.ts +62 -416
- package/src/protocol.ts +0 -6
- package/src/providers/index.ts +8 -4
- package/src/providers/local/provider.ts +259 -87
- package/src/providers/ssh/provider.ts +158 -155
- package/src/remote/environment.ts +22 -1
- package/src/remote/remote-fs.ts +123 -34
- package/src/remote/remote-process.ts +265 -203
- package/src/remote/remote-subprocess.ts +61 -18
- package/src/remote/remote-terminal.ts +106 -21
- package/src/remote-search.ts +122 -26
- package/src/routes.ts +416 -395
- package/src/runtime/dsh-capabilities.ts +19 -0
- package/src/runtime/workspace-core.ts +171 -88
- package/src/runtime/workspace-migration.ts +472 -0
- package/src/ssh/connection/lease.ts +35 -0
- package/src/ssh/connection/manager.ts +1083 -0
- package/src/ssh/connection/pool.ts +341 -275
- package/src/ssh/engine.ts +269 -1477
- package/src/ssh/known-hosts.ts +42 -18
- package/src/ssh/local-transfer-policy.ts +83 -0
- package/src/ssh/plugin.ts +20 -19
- package/src/ssh/protocol.ts +22 -1
- package/src/ssh/routes.ts +419 -187
- package/src/ssh/sftp/service.ts +967 -0
- package/src/ssh/store.ts +169 -72
- package/src/ssh/terminal/service.ts +177 -0
- package/src/ssh/tools.ts +42 -20
- package/src/ssh/tunnel/service.ts +217 -0
- package/src/ssh/vault.ts +245 -91
- package/src/subprocess.ts +88 -71
- package/src/switch/switch-fs.ts +141 -10
- package/src/switch/switch-subprocess.ts +80 -6
- package/src/tools.ts +217 -221
- package/src/workspace-tool-ops.ts +101 -0
- package/src/workspace.ts +52 -0
- package/lib/environment-BL1jddfB.js +0 -449
- package/lib/switch-fs-CAJpFY9C.js +0 -193
- package/lib/switch-fs-RrZtG2gv.js +0 -210
- package/lib/types/client/manager-button.d.ts +0 -32
- package/lib/types/client/ssh/mount.d.ts +0 -13
- package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
- package/lib/types/client/ssh/panel/controller.d.ts +0 -23
- package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
- package/lib/types/client/workspace-gate.d.ts +0 -15
- package/lib/types/remote-runner.d.ts +0 -83
- package/lib/types/seam-state.d.ts +0 -69
- package/src/client/manager-button.tsx +0 -269
- package/src/client/ssh/mount.tsx +0 -83
- package/src/client/ssh/panel/HostsTab.tsx +0 -236
- package/src/client/ssh/panel/controller.ts +0 -48
- package/src/client/ssh/sidebar-entry.ts +0 -123
- package/src/client/workspace-gate.ts +0 -225
- package/src/remote-runner.ts +0 -201
- package/src/seam-state.ts +0 -185
package/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, tiphareth0
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
23
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
24
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
25
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
26
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
27
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
28
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
29
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
30
|
+
POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
CHANGED
|
@@ -1,50 +1,77 @@
|
|
|
1
|
-
# dsh-hardssh — SSH 远程工作区 + SSH 运维插件
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
1. **SSH
|
|
6
|
-
2. **SSH
|
|
7
|
-
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
1
|
+
# dsh-hardssh — SSH 远程工作区 + SSH 运维插件
|
|
2
|
+
|
|
3
|
+
已适配 DSH **0.1.5**(实测内核 `0.1.5-rc.1`;本包版本 `0.2.3`)。在 DSH Web GUI 中提供两块能力(单包单引擎):
|
|
4
|
+
|
|
5
|
+
1. **SSH 运维**:右侧栏「SSH」Tab(从右侧栏标签条的「+」或右侧栏引导页入口打开)→ Web 终端(xterm + WebSocket PTY)、文件上传下载、本地端口转发隧道、当前服务器的远端命令;`ssh_list` / `ssh_exec` / `ssh_upload` / `ssh_download` / `ssh_tunnel` / `ssh_cluster` 六个 Agent 工具;主机配置存 `~/.dsh/dsh-ssh.json`。
|
|
6
|
+
2. **SSH 工作区**:左侧侧栏的全局入口行 → 中央面板管理服务器与工作区(增删改查 / `~/.ssh/config` 导入);绑定后本地 harness 的 fs/subprocess 经接缝门面透明路由到远程主机执行(read/write/edit/bash 在绑定会话中即远程操作);`remote_*` 三个 Agent 工具(`remote_ls` / `remote_search` / `remote_status`)用于显式操作远端工作区。
|
|
7
|
+
|
|
8
|
+
## 核心优势
|
|
9
|
+
|
|
10
|
+
- **对插件零改动**:`cordis.patch.yml` 禁用部署自带的 `fs-sandbox` / `subprocess` 行,由本包提供路由门面。任何走标准 `ctx.fs` / `ctx.subprocess` 的插件与标准工具,在 SSH 工作区会话里自动运行在远端。
|
|
11
|
+
- **通用工作区底座**:`WorkspaceRecord` / `Provider` / `Connection` / 能力契约 + Registry / Ledger / Router,与 SSH 解耦。SSH 只是一个 provider(如 `ssh`、`local`),可继续接 docker / wsl / 云 devbox,上层插件与 UI 不改;单一运行时,全链路读同一个台账。
|
|
12
|
+
- **少数插件只需改几处接口**:给 agent 的执行手册见 [`SKILLS.md`](./SKILLS.md)——判定命令、接口对照表、可照抄代码与自检清单。
|
|
13
|
+
|
|
14
|
+
## 架构
|
|
15
|
+
|
|
16
|
+
- **单一共享实例**:`HostStore` + `SshEngine`(ssh2 连接池)在 `src/index.ts` 创建一次,SSH 运维与 SSH 工作区共用同一引擎 —— 配置变更(PATCH/DELETE)同时失效所有连接,无双池问题。
|
|
17
|
+
- **接缝切换**:`cordis.patch.yml` 禁用部署自带的 `fs-sandbox` / `subprocess` 行,由 `dsh-hardssh/fs`、`dsh-hardssh/subprocess` 提供 provider 路由门面(本地 = 沙箱化原实现;远端 = 该 workspace 连接上的 `workspace.fs` / `workspace.process` capability)。
|
|
18
|
+
- **REST**:`/api/dsh-ssh`(运维路由,loopback-only)+ `/api/dsh-hardssh`(工作区路由,loopback-only)。
|
|
19
|
+
- **工作区核心服务**:`ctx.workspaceCore`(通用 WorkspaceCore:台账 + provider 路由 + capability 连接)是**唯一**的工作区运行时;`ctx.hardsshCore` 只保留 `hosts` + `engine` 供 SSH 专用集成消费(如四列 IDE 形态的 `dsh-workbench-tiphareth`)。
|
|
20
|
+
- **公开入口**:`@tiphareth/dsh-hardssh/base`(通用底座实现)、`@tiphareth/dsh-hardssh/workspace`(平台无关类型面)。
|
|
21
|
+
|
|
22
|
+
## 界面入口(全部走标准插件扩展点)
|
|
23
|
+
|
|
24
|
+
插件不使用任何 DOM 注入;两个界面都是内核公开的槽位注册:
|
|
25
|
+
|
|
26
|
+
| 界面 | 入口 | 槽位 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| **SSH 工作区管理**(服务器 + 工作区增删改查,服务器行带已连接/未连接徽章) | 左侧侧栏「新会话」与「工作区」之间的全局入口行 → 中央面板 | `sidebar.panellist`(行)+ `main`(面板,key 同为 `dsh-hardssh-workspaces`) |
|
|
29
|
+
| **SSH 运维**(终端 / 传输 / 隧道 / 当前服务器命令) | 右侧栏标签条的「+」或右侧栏引导页入口 | `ctx.sidebarRightTabs.register`(类型)+ `sidebar.right.pane.tab`(正文)+ `sidebar.right.pane.tab.title`(标签文字) |
|
|
30
|
+
|
|
31
|
+
右侧栏 Tab 是 **page 类型**(不声明 `patterns`),只按 kind 打开,由用户从右侧栏自己的入口打开;插件不会强制展开右侧栏。右侧栏 Tab 实例是**每会话独立**的(内核的会话作用域语义),因此切换会话后需要重新打开该 Tab。
|
|
32
|
+
|
|
33
|
+
## 会话绑定语义(操作台不选服务器)
|
|
34
|
+
|
|
35
|
+
- 操作台的 SSH 目标由**当前会话的 `cwd`** 决定:最长匹配的 SSH 工作区锚点胜出,取该工作区的 `alias` 与 `remoteRoot`。终端 / 传输 / 隧道 / 命令四个子页都强制使用它,**不提供服务器下拉框**。
|
|
36
|
+
- 会话在本地工作区(或未绑定任何 SSH 工作区)时,操作台不挂载任何操作组件,改为渲染**模糊蒙版**并提示「SSH 操作台仅适用于 SSH 工作区会话」。
|
|
37
|
+
- 切换会话时操作台随 `Session → alias` 自动切换,并重置子页状态。
|
|
38
|
+
- 数据源是公开的 `ctx.sessions.list`(`current` + `byId[id].cwd`)+ `WorkspaceManager` 快照,两者都可订阅,不存在第二份工作区句柄。
|
|
39
|
+
|
|
40
|
+
## 连接行为
|
|
41
|
+
|
|
42
|
+
- **启动/刷新只连接当前会话的服务器**:连接闸门在 `ctx.sessions.list` 的 `phase === 'ready'` 之前不建立基准,避免历史会话被误判为「新建会话」而逐台探测;本地会话不触发任何连接。
|
|
43
|
+
- **非交互失败可见**:探测失败(网络不可达、认证失败、主机密钥异常、重试耗尽)会弹出「无法连接服务器」对话框并显示具体原因;用户主动取消密码/指纹弹窗不算失败。
|
|
44
|
+
- **状态徽章只读**:左侧面板每 3 秒读取 `/api/dsh-ssh/connections`(连接池 live alias 列表)刷新「已连接 / 未连接」徽章,**不会**主动拨号。
|
|
45
|
+
|
|
46
|
+
## 安装
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# 已发布 npm(当前 0.2.3):profile 机制,热插拔,需重启 dsh
|
|
50
|
+
dsh plugin --profile <name> add @tiphareth/dsh-hardssh
|
|
51
|
+
|
|
52
|
+
# 开发/迭代:源码链接(改码重建 lib/ 后重启即生效)
|
|
53
|
+
dsh plugin --profile <name> add link:<repo>/packages/dsh-hardssh
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 开发
|
|
57
|
+
|
|
58
|
+
在仓库根目录执行:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
pnpm --filter dsh-hardssh typecheck # 类型检查
|
|
62
|
+
pnpm test # 默认测试套件(vault 加密用例已移出,约 12s)
|
|
63
|
+
pnpm test:vault # 只跑 vault 用例(约 21s,scrypt 派生故意慢)
|
|
64
|
+
pnpm --filter dsh-hardssh build # 产出 lib/(构建前先清空,避免陈旧产物)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> profile 通过 `link:` 指向本包源码目录,因此改码后只需重新 `build`(产出 `lib/`)并重启
|
|
68
|
+
> `dsh web` 即生效;不要把依赖改成 `file:` 安装副本,那会让 profile 加载一份快照而不是源码。
|
|
69
|
+
|
|
70
|
+
## 安全模型
|
|
71
|
+
|
|
72
|
+
- `/api/dsh-ssh/*` 与 `/api/dsh-hardssh/*` 仅限 loopback(含同源校验)。
|
|
73
|
+
- 认证材料沿用 `~/.dsh/dsh-ssh.json`(0600 / 0700),不新增存储。
|
|
74
|
+
- 路径 gate:远程操作 root 必须等于 resolved remoteRoot;相对路径禁止 `..`;`workspace.fs` / `workspace.process` capability 在解析后的 canonical 路径上再做一次 root 收敛(symlink 逃逸 fail closed)。
|
|
75
|
+
- 远程操作消耗真实远程资源:工具描述与宣告段明确「先确认再执行」;`remote_search` 有深度与条数上限;`glob` / `grep` 在 SSH 会话里会被**显式拒绝**(本机 ripgrep 读不到服务器内容)并指向 `remote_search`。
|
|
76
|
+
- SSH 模式下本机沙箱不对远程执行生效(远程进程无法被本地内核沙箱约束):门面的 `sandboxMode` 委托本地后端的真实模式(`write` / `edit` 的沙箱升级入口据此注册),而远端世界的升级策略在门面处**显式丢弃**。
|
|
77
|
+
- 凭据默认不落盘;`secretStorage: vault` 时以 AES-256-GCM + scrypt 加密存储于 `~/.dsh/ssh-secrets/dsh-ssh-vault.json`(被 fs seam 拒绝访问),且 `DSH_CREDENTIAL_PASSWORD` 自动解锁默认关闭(需 `vaultAutoUnlock: env`)。会话密码按**连接存活期**复用,连接池回收即失效。
|
package/SKILLS.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# SKILLS — 把任意 DSH 插件适配到 SSH 工作区
|
|
2
|
+
|
|
3
|
+
> 写给 agent 的执行手册。目标:用**最少的接口替换**,让一个插件在 SSH 工作区及其 session 下正常工作。
|
|
4
|
+
> 底座包:`@tiphareth/dsh-hardssh`(本包)。参考实现:`src/client/session-connect-gate.ts`、`src/client/ssh/session-target.ts`、`src/providers/ssh/provider.ts`。
|
|
5
|
+
|
|
6
|
+
## 0. 先判断属于哪一类(决定要不要改)
|
|
7
|
+
|
|
8
|
+
| 插件的做法 | 结论 |
|
|
9
|
+
|---|---|
|
|
10
|
+
| 通过 `ctx.fs` / `ctx.subprocess`(DSH 标准服务)或标准工具(read / write / edit / bash / pwsh…)访问文件与进程 | **0 改动**。seam 已被替换,路由自动生效 |
|
|
11
|
+
| 依赖 **glob / grep** 做检索 | 这两个工具走**本机打包的 ripgrep**(不经 `ctx.fs`),在远端会话里读不到服务器内容、会被**显式拒绝**;远端检索改用 `remote_search`(`mode="glob"` / `mode="grep"`)、`remote_ls`、`ssh_exec` |
|
|
12
|
+
| 直接 `import 'node:fs'` / `node:child_process`,或自建「本地/远端」状态、自建 workspace 句柄 | 需要改接口,按第 1~3 节替换 |
|
|
13
|
+
|
|
14
|
+
判定命令(在插件源码根执行):
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
rg -n "node:fs|node:child_process|from 'fs'|from 'child_process'|spawn\(|execFile\(" src
|
|
18
|
+
rg -n "remote|isRemote|mode\s*[:=].*local" src
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
命中即按对应小节替换。
|
|
22
|
+
|
|
23
|
+
## 1. 文件 / 进程:改用底座已替换的 seam
|
|
24
|
+
|
|
25
|
+
**把** 直接的 Node 文件与进程调用 **改为** DSH 标准服务:
|
|
26
|
+
|
|
27
|
+
| 现在 | 改为 | 导入 |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `node:fs`、`node:fs/promises` | `ctx.fs`(`FileSystem`) | `@deepseek-ai/dsh-fs` |
|
|
30
|
+
| `node:child_process`、`spawn`、`exec` | `ctx.subprocess`(`SubprocessRuntime`) | `@deepseek-ai/dsh-subprocess` |
|
|
31
|
+
|
|
32
|
+
改完后 **不需要**任何 SSH / 远端分支:`dsh-hardssh` 已通过 `cordis.patch.yml` 把这两个 seam 替换为路由门面,按 session 的 cwd 决定本机还是远端。
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// before
|
|
36
|
+
import { readFile } from 'node:fs/promises'
|
|
37
|
+
const text = await readFile(target, 'utf8')
|
|
38
|
+
|
|
39
|
+
// after —— ctx.fs 是 DSH FileSystem:先 resolve 得到 FsTarget,再读写
|
|
40
|
+
const file = await ctx.fs.resolve(target) // 远端会话自动解析到远端
|
|
41
|
+
const text = await ctx.fs.readText(file)
|
|
42
|
+
await ctx.fs.writeText(file, next)
|
|
43
|
+
|
|
44
|
+
// before
|
|
45
|
+
import { execFile } from 'node:child_process'
|
|
46
|
+
|
|
47
|
+
// after —— ctx.subprocess 是 DSH SubprocessRuntime:spawn 同步返回句柄,done 给结果
|
|
48
|
+
const handle = ctx.subprocess.spawn({ argv: ['bash', '-lc', cmd], cwd })
|
|
49
|
+
const outcome = await handle.done
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 2. 工作区身份:改用 `workspaceCore`,不要自建映射
|
|
53
|
+
|
|
54
|
+
**把** 插件自己的「路径 → 远端 / 模式」映射与自建 workspace 句柄 **改为** 通用底座的查询接口:
|
|
55
|
+
|
|
56
|
+
| 现在 | 改为 | 位置 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| 自己维护 path→远端映射 | `ctx.workspaceCore.findByAnchor(path)` / `openByAnchor(path)` | 底座公开面 |
|
|
59
|
+
| 需要「这个 cwd 属于哪个工作区」的连接 | `ctx.workspaceCore.openByAnchor(cwd)` → `WorkspaceConnection` | 同上 |
|
|
60
|
+
| 自己的 workspace 句柄类型 | `WorkspaceCore` / `WorkspaceConnection` / `WorkspaceRecord` | `@tiphareth/dsh-hardssh/workspace` |
|
|
61
|
+
| 自己实现的文件/进程能力契约 | `connection.get('workspace.fs' \| 'workspace.process' \| 'workspace.search')` | 同上 |
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
export const inject = ['workspaceCore'] // 声明依赖
|
|
65
|
+
|
|
66
|
+
const record = await ctx.workspaceCore.findByAnchor(cwd) // 未命中 → 本地
|
|
67
|
+
if (record !== undefined) {
|
|
68
|
+
const connection = await ctx.workspaceCore.openByAnchor(cwd)
|
|
69
|
+
const remoteRoot = record.location.root // 远端根
|
|
70
|
+
const fs = connection?.get('workspace.fs') // DSH FileSystem
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
要点:
|
|
75
|
+
|
|
76
|
+
- `findByAnchor` / `openByAnchor` 会先等待运行时就绪,是**首选**读写路径。
|
|
77
|
+
- 已经持有连接时,用 `record.id` 走 `openById(record.id)` 复用同一条连接,不要重新按路径解析。
|
|
78
|
+
- 需要在渲染热路径上做**同步**判断时,底座内部用的是 `core.router.fromAnchor(cwd)`(seam 门面即如此)。
|
|
79
|
+
该属性在 `WorkspaceCore` 上标注为**内部兼容面**(`@deprecated`),插件应优先用异步高阶层;
|
|
80
|
+
确实需要同步判定(例如渲染期不能 await)才使用它,并集中封装在一个适配函数里。
|
|
81
|
+
- 未就绪或未命中时**不要**猜测:未命中即当本地处理;锚点目录下未注册的路径应 fail closed。
|
|
82
|
+
- `WorkspaceRecord.provider.id` 是 provider 类型(`ssh` / `local` / …),用它做能力判断,**不要**用 alias 当身份。
|
|
83
|
+
- SSH 工作区的服务器别名在 `record.provider.connectionRef.id`(`alias` 只是展示用);远端根在 `record.location.root`。
|
|
84
|
+
|
|
85
|
+
## 3. 注册新 provider(只有要接新后端时才做)
|
|
86
|
+
|
|
87
|
+
**把** 自建的后端注册 **改为** 底座的 provider 契约:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const provider: WorkspaceProvider = {
|
|
91
|
+
manifest: {
|
|
92
|
+
id: 'docker', version: '1.0.0',
|
|
93
|
+
apiVersion: WORKSPACE_PROVIDER_API_VERSION, // 当前 = 2
|
|
94
|
+
displayName: 'Docker',
|
|
95
|
+
capabilities: ['workspace.fs', 'workspace.process', 'workspace.search'],
|
|
96
|
+
},
|
|
97
|
+
validate(record) { /* 校验 root / ref 可解析,不碰网络 */ },
|
|
98
|
+
async open(record, context) { /* 返回 WorkspaceConnection:get()/status()/close() */ },
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const dispose = ctx.workspaceCore.registerProvider(provider)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`get()` 的返回值必须直接用 DSH 官方类型(`FileSystem` / `SubprocessRuntime`)或底座能力类型(`WorkspaceSearchService`),这样所有消费方无需分支。
|
|
105
|
+
|
|
106
|
+
## 4. 客户端(浏览器侧):按 session 取远端身份
|
|
107
|
+
|
|
108
|
+
**把** 自建的「当前是否远端 / 当前连哪台」状态 **改为** 从公开的 session 列表 + 工作区快照推导:
|
|
109
|
+
|
|
110
|
+
| 现在 | 改为 |
|
|
111
|
+
|---|---|
|
|
112
|
+
| 全局的 local/remote 开关 | `ctx.sessions.list.getSnapshot()` 的 `current` + `byId[id].cwd` |
|
|
113
|
+
| 自建的 alias / 远端选择器 | 最长匹配的 workspace anchor → `workspace.alias` / `workspace.remoteRoot` |
|
|
114
|
+
| 各自轮询工作区列表 | 共享一个 `WorkspaceManager` 快照(订阅即可,不要二次拉取) |
|
|
115
|
+
|
|
116
|
+
参考实现(照抄这两个文件即可):
|
|
117
|
+
|
|
118
|
+
- `src/client/session-connect-gate.ts` — `SessionGateList` 适配层 + `makeAnchorWorkspaceResolver()`(最长锚点匹配、Windows 大小写折叠)+ 打开会话时探测服务器。
|
|
119
|
+
- `src/client/ssh/session-target.ts` — `createSessionSshTargetSource()`:`Session → 固定远端目标` 的可订阅数据源(`useSyncExternalStore` 友好,快照引用稳定)。
|
|
120
|
+
|
|
121
|
+
约定:
|
|
122
|
+
|
|
123
|
+
- 面板 / Tab 的目标**跟随 session**,不要在面板里放服务器下拉框(切换服务器 = 切换 session)。
|
|
124
|
+
- session 未绑定工作区时,渲染明确的不可用状态(例如模糊蒙版),不要静默失败或回退到本机操作。
|
|
125
|
+
|
|
126
|
+
## 5. 自检清单
|
|
127
|
+
|
|
128
|
+
1. `rg "node:fs|node:child_process" src` 无生产命中(测试除外)。
|
|
129
|
+
2. 不再存在自建的 path→远端映射或 local/remote 全局状态。
|
|
130
|
+
3. 所有工作区查询都经 `ctx.workspaceCore`;provider 身份判断用 `record.provider.id`。
|
|
131
|
+
4. 在 SSH 工作区 session 中执行插件核心功能 → 落在远端(用 `remote_*` 工具或远端 `pwd` 复核)。
|
|
132
|
+
5. 在本地 session 中执行同一功能 → 行为与改造前一致。
|
|
133
|
+
6. 单元测试里用假 `WorkspaceCore` 注入即可,无需真实 SSH。
|
|
134
|
+
|
|
135
|
+
## 6. 不要做
|
|
136
|
+
|
|
137
|
+
- 不要在插件里直接建 SSH 连接 —— 连接、凭据、TOFU 都由底座的引擎统一管理。
|
|
138
|
+
- 不要缓存 `WorkspaceRecord` 当作长期身份 —— 用 `id` + `findByAnchor()` 重新解析。
|
|
139
|
+
- 不要用本机 `realpath` 解析远端路径 —— 交给 `workspace.fs` capability。
|
|
140
|
+
- 不要假设锚点目录存在或可写 —— 它只是 session cwd 的占位锚点。
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { a as normalizeAnchorPath, i as isPathUnderAnchor, n as WorkspaceLedger, r as assertValidRecords, t as WORKSPACE_LEDGER_SCHEMA_VERSION } from "../ledger-D2ezq1iW.js";
|
|
2
|
+
import { a as decodeLegacySshKey, c as defaultNamespaceCodec, i as WFS_SCHEME, l as encodeRoute, n as WorkspaceProviderRegistry, o as decodeWfsKey, r as LedgerWorkspaceRouter, s as decodeWorkspaceId, t as ProviderRegistrationError, u as encodeWorkspaceId } from "../registry-CViuzKYI.js";
|
|
3
|
+
import { t as WORKSPACE_PROVIDER_API_VERSION } from "../model-Cp7f70Mb.js";
|
|
4
|
+
//#region src/base/plugin.ts
|
|
5
|
+
/**
|
|
6
|
+
* Registry of loaded plugins (provider + feature plugins together). Keeps
|
|
7
|
+
* the workspace registry and plugin lifecycle in one place.
|
|
8
|
+
*
|
|
9
|
+
* NOT WIRED INTO PRODUCTION YET: the DSH runtime registers the builtin
|
|
10
|
+
* providers directly (`runtime/workspace-core.ts`) and no third-party plugin
|
|
11
|
+
* loads through this host — only tests do. It is kept because it is the
|
|
12
|
+
* documented distribution contract for out-of-tree providers (see SKILLS.md);
|
|
13
|
+
* provider teardown stays ownership-correct for when it is wired up: each
|
|
14
|
+
* registration releases the disposer that `register()` returned for THAT call,
|
|
15
|
+
* so a duplicate (same id+version) registration never unregisters a provider it
|
|
16
|
+
* does not own.
|
|
17
|
+
*
|
|
18
|
+
* @internal Not reachable from any production assembly today.
|
|
19
|
+
*/
|
|
20
|
+
var WorkspacePluginHost = class {
|
|
21
|
+
registries;
|
|
22
|
+
plugins = /* @__PURE__ */ new Map();
|
|
23
|
+
features = /* @__PURE__ */ new Set();
|
|
24
|
+
/** Per-plugin disposers collected from activate (provider/feature/workspace listeners). */
|
|
25
|
+
pluginDisposers = /* @__PURE__ */ new Map();
|
|
26
|
+
constructor(registries) {
|
|
27
|
+
this.registries = registries;
|
|
28
|
+
}
|
|
29
|
+
/** Load a plugin: validate manifest, run activate. Throws on failure. */
|
|
30
|
+
async load(plugin) {
|
|
31
|
+
if (this.plugins.has(plugin.manifest.id)) throw new Error(`plugin '${plugin.manifest.id}' already loaded`);
|
|
32
|
+
if (plugin.manifest.apiVersion !== 1) throw new Error(`plugin '${plugin.manifest.id}' apiVersion ${plugin.manifest.apiVersion} is not supported (need 1)`);
|
|
33
|
+
const disposers = [];
|
|
34
|
+
await plugin.activate({
|
|
35
|
+
registerProvider: (provider) => {
|
|
36
|
+
const disposer = this.registerProvider(provider);
|
|
37
|
+
disposers.push(disposer);
|
|
38
|
+
return { dispose: disposer };
|
|
39
|
+
},
|
|
40
|
+
registerFeature: (feature) => {
|
|
41
|
+
const disposer = () => {
|
|
42
|
+
this.features.delete(feature);
|
|
43
|
+
};
|
|
44
|
+
disposers.push(disposer);
|
|
45
|
+
this.features.add(feature);
|
|
46
|
+
return { dispose: disposer };
|
|
47
|
+
},
|
|
48
|
+
workspaces: this.registries.workspaces,
|
|
49
|
+
onWorkspaceChange: (listener) => {
|
|
50
|
+
const disposer = this.registries.workspaces.subscribe(listener);
|
|
51
|
+
disposers.push(disposer);
|
|
52
|
+
return { dispose: disposer };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
this.pluginDisposers.set(plugin.manifest.id, disposers);
|
|
56
|
+
this.plugins.set(plugin.manifest.id, plugin);
|
|
57
|
+
}
|
|
58
|
+
/** Unload a plugin, disposing every contribution (providers, features, listeners). */
|
|
59
|
+
async unload(id) {
|
|
60
|
+
const plugin = this.plugins.get(id);
|
|
61
|
+
if (plugin === void 0) return;
|
|
62
|
+
await plugin.deactivate?.();
|
|
63
|
+
const disposers = this.pluginDisposers.get(id) ?? [];
|
|
64
|
+
for (const dispose of disposers) try {
|
|
65
|
+
dispose();
|
|
66
|
+
} catch {}
|
|
67
|
+
this.pluginDisposers.delete(id);
|
|
68
|
+
this.plugins.delete(id);
|
|
69
|
+
}
|
|
70
|
+
registerProvider(provider) {
|
|
71
|
+
const disposeRegistration = this.registries.workspaces.register(provider);
|
|
72
|
+
let disposed = false;
|
|
73
|
+
return () => {
|
|
74
|
+
if (disposed) return;
|
|
75
|
+
disposed = true;
|
|
76
|
+
disposeRegistration();
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Active features (for a runtime to wire up per-workspace surfaces). */
|
|
80
|
+
featuresFor(workspace, capabilities) {
|
|
81
|
+
return [...this.features].filter((feature) => feature.supports({
|
|
82
|
+
workspace,
|
|
83
|
+
capabilities
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Minimal capability set surface used by the DSH runtime to decide which
|
|
89
|
+
* features apply to a workspace. Computed from the provider manifest.
|
|
90
|
+
*/
|
|
91
|
+
function capabilitiesOf(provider) {
|
|
92
|
+
return new Set(provider.manifest.capabilities);
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { LedgerWorkspaceRouter, ProviderRegistrationError, WFS_SCHEME, WORKSPACE_LEDGER_SCHEMA_VERSION, WORKSPACE_PROVIDER_API_VERSION, WorkspaceLedger, WorkspacePluginHost, WorkspaceProviderRegistry, assertValidRecords, capabilitiesOf, decodeLegacySshKey, decodeWfsKey, decodeWorkspaceId, defaultNamespaceCodec, encodeRoute, encodeWorkspaceId, isPathUnderAnchor, normalizeAnchorPath };
|