dsh-remote-workspaces 0.1.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/LICENSE +21 -0
- package/README.md +149 -0
- package/README.zh.md +151 -0
- package/cordis.patch.yml +25 -0
- package/package.json +44 -0
- package/src/anchor.js +67 -0
- package/src/client.js +756 -0
- package/src/containment.js +71 -0
- package/src/errors.js +17 -0
- package/src/fs-sftp.js +231 -0
- package/src/index.js +327 -0
- package/src/local-backend.js +224 -0
- package/src/machine-store.js +238 -0
- package/src/registry.js +76 -0
- package/src/routing-fs.js +230 -0
- package/src/search.js +286 -0
- package/src/shell-exec.js +423 -0
- package/src/ssh-config.js +52 -0
- package/src/ssh-uri.js +19 -0
- package/src/transport.js +401 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-remote-workspaces contributors
|
|
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
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# dsh-remote-workspaces
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Open folders on remote hosts over SSH as first-class [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) workspaces.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Once a remote directory is opened as a workspace, the agent's file tools (`read` / `write` / `edit` / `grep` / `glob`) and shell commands land **directly on the remote host** over SSH, not a local mirror. There is no mirroring and no sync: every operation is executed remotely in real time, and local workspaces keep their full sandbox behavior.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Open a remote folder as a workspace** — browse the remote filesystem, pick a directory, and the harness adopts it as if it were local. All I/O routes to the remote.
|
|
14
|
+
- **Multi-machine registry** — add, edit, and remove SSH hosts (alias, host, port, user, key or password). Credentials are AES-256-GCM encrypted at rest and never sent back to the browser.
|
|
15
|
+
- **`~/.ssh/config` import** — list configured aliases and pre-fill the connection form.
|
|
16
|
+
- **Connection test** — verify a host before using it.
|
|
17
|
+
- **Transparent tool routing** — `read`/`write`/`edit` go through SFTP, `grep`/`glob` run ripgrep on the remote, and `bash`/`pwsh` commands execute over ssh2 exec. Local paths keep the harness's own sandbox.
|
|
18
|
+
- **Policy-aware remote mutations** — the harness file policy applies to remote workspaces too: `read-only` denies `write`/`edit` and remote shell commands, `workspace-write` confines `write`/`edit` to the remote workspace root (plus `/tmp`) while remote shell commands need a `danger-full-access` approval, and `danger-full-access` delegates to the SSH account.
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
The bundle has a host half and a browser half:
|
|
23
|
+
|
|
24
|
+
- **Host** (`src/index.js`) — an SSH/SFTP transport built on [`ssh2`](https://github.com/mscdex/ssh2), an encrypted machine store, the routing filesystem (`ctx.fs`) and shell (`ctx.shell`) providers, remote-aware `grep`/`glob` tools, and a `remoteWorkspaces` Remote namespace exposed through Typert.
|
|
25
|
+
- **Browser** (`src/client.js`) — a "Remote Workspaces" settings section (hosts grouped by machine) and a workspace-add picker.
|
|
26
|
+
|
|
27
|
+
Opening a remote directory creates an **anchor**: a real but empty local directory adopted by the harness as the workspace identity, plus a metadata file recording its remote origin. A registry (`anchors.json`) maps that anchor to its remote host and path, and every file/command operation is routed to the remote by that lookup:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
$DSH_HOME/remote-workspaces/
|
|
31
|
+
├── machines.json # host registry (credentials encrypted at rest)
|
|
32
|
+
├── anchors.json # anchor → remote origin routing registry
|
|
33
|
+
└── <host>-<user>-<port>/ # per-host anchors
|
|
34
|
+
└── <encoded-path>/ # one empty anchor per remote path
|
|
35
|
+
│ └── .dsh-remote-meta.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The full remote path is encoded into the anchor name (path separators and Windows-illegal characters become `--`), so `/home/test` and `/data/test` map to distinct directories (`home--test` vs `data--test`).
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- A DeepSeek Harness installation (the plugin resolves its host services at runtime).
|
|
43
|
+
- Node.js ≥ 18 (the harness itself runs on Node 22+).
|
|
44
|
+
- `ssh2` is the only transport dependency; no external `ssh` binary is needed.
|
|
45
|
+
- The remote host needs `rg` (ripgrep) for `grep`/`glob`, and `sha256sum` (or `shasum`) for post-write verification — both degrade gracefully when absent.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
Install from this repository:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
dsh plugin --profile web add github:januory/dsh-remote-workspaces
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Once published to npm, install the released package directly:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
dsh plugin --profile web add dsh-remote-workspaces
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Install from source:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
git clone https://github.com/januory/dsh-remote-workspaces.git
|
|
65
|
+
|
|
66
|
+
cd dsh-remote-workspaces
|
|
67
|
+
|
|
68
|
+
pnpm install # install the ssh2 transport dependency
|
|
69
|
+
|
|
70
|
+
dsh plugin --profile web add .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Remove it with:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
dsh plugin --profile web remove dsh-remote-workspaces
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> Maintainer note: the npm package name is now `dsh-remote-workspaces` (the early git install used `remote-workspaces`); older installs must first run `dsh plugin --profile <name> remove remote-workspaces`, then install under the new name.
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
1. Open **Settings → Remote Workspaces**.
|
|
84
|
+
2. Add an SSH host (or import one from `~/.ssh/config`), then **Test connection**.
|
|
85
|
+
3. In the workspace-add flow, choose a remote host, browse to a directory, and open it. The harness adopts the empty anchor as a workspace, and reads, writes, searches, and shell commands all execute on the remote.
|
|
86
|
+
|
|
87
|
+
## Remote API
|
|
88
|
+
|
|
89
|
+
The host exposes a `remoteWorkspaces` Remote namespace (Typert) with these invocations: `listMachines`, `saveMachine`, `deleteMachine`, `listSshAliases`, `sshAliasDetail`, `testConnection`, `listRemoteDir`, and `openRemoteWorkspace`.
|
|
90
|
+
|
|
91
|
+
## Repository structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
src/ # the DSH bundle source
|
|
95
|
+
index.js # host entry (routing fs/shell, search tools, Remote namespace)
|
|
96
|
+
client.js # browser entry (settings UI + picker)
|
|
97
|
+
transport.js # ssh2 transport (SshClient, exec, sha256)
|
|
98
|
+
routing-fs.js # routing filesystem (remote SFTP / local fence)
|
|
99
|
+
fs-sftp.js # SFTP filesystem backend
|
|
100
|
+
local-backend.js # local filesystem backend
|
|
101
|
+
containment.js # local sandbox containment fence
|
|
102
|
+
shell-exec.js # SshShellExecutor (remote ssh2 exec / local subprocess)
|
|
103
|
+
search.js # remote-aware grep/glob tools
|
|
104
|
+
anchor.js # local anchor layout (empty dir + meta)
|
|
105
|
+
registry.js # anchors.json routing registry
|
|
106
|
+
machine-store.js # host registry (encrypted at rest)
|
|
107
|
+
ssh-config.js # ~/.ssh/config parser
|
|
108
|
+
ssh-uri.js # ssh:// URI parsing + detection
|
|
109
|
+
errors.js # error codes
|
|
110
|
+
cordis.patch.yml # DSH bundle patch (swap in the routing providers)
|
|
111
|
+
package.json # package + dsh manifest
|
|
112
|
+
test/ # unit + integration tests
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
116
|
+
|
|
117
|
+
Pure unit tests (no host required) run anywhere:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
pnpm test
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The integration suite exercises a real SSH host and expects the machine registry to be configured first (it targets the first machine in the registry, or the one aliased `test`):
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
pnpm test:integration
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Publishing (maintainers)
|
|
130
|
+
|
|
131
|
+
Releases are fully manual with **zero input**: the version comes straight from `package.json` — commit the new `version` on `main` first, then GitHub Actions → **release** → **Run workflow**. The workflow runs the unit tests (`npm test`), guards that the `v<version>` tag does not exist yet (prevents duplicate releases), publishes to npm with provenance (`npm publish --provenance`), creates and pushes the `v<version>` tag, and creates a GitHub Release with `--generate-notes`.
|
|
132
|
+
|
|
133
|
+
Prerequisites: configure an `NPM_TOKEN` secret in **Settings → Secrets and variables → Actions** (an npm automation token, or a granular token with publish permission on the package); the publishing account must be the npm user owning the package name.
|
|
134
|
+
|
|
135
|
+
Equivalent manual flow:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npm login
|
|
139
|
+
|
|
140
|
+
npm publish --provenance
|
|
141
|
+
|
|
142
|
+
git tag "v$(node -p \"require('./package.json').version\")"
|
|
143
|
+
|
|
144
|
+
git push origin --tags
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
[MIT](./LICENSE)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# dsh-remote-workspaces
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
通过 SSH 把远程主机上的目录,像打开本地文件夹一样,作为[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的工作区打开工作。
|
|
6
|
+
|
|
7
|
+
## 它做了什么
|
|
8
|
+
|
|
9
|
+
远程目录一旦被打开为工作区,agent 的文件工具(`read` / `write` / `edit` /`grep` / `glob`)和 shell 命令都会**直接落到远主机**上执行,而不是本地镜像。这里没有镜像、没有同步:每个操作都在远端实时执行,而本地工作区仍保留 harness 自身的完整沙箱行为。
|
|
10
|
+
|
|
11
|
+
## 功能特性
|
|
12
|
+
|
|
13
|
+
- **把远程目录当作工作区打开** —— 浏览远程文件系统、选中目录,harness 会像对待本地目录一样接纳它;所有 I/O 都路由到远端。
|
|
14
|
+
- **多主机注册表** —— 增删改 SSH 主机(别名 / 主机 / 端口 / 用户 / 密钥或密码)。凭据在本地用 AES-256-GCM 加密落盘,绝不会回传到浏览器。
|
|
15
|
+
- **`~/.ssh/config` 导入** —— 列出已配置的别名并自动填充连接表单。
|
|
16
|
+
- **连接测试** —— 使用前先验证主机连通性。
|
|
17
|
+
- **透明的工具路由** —— `read`/`write`/`edit` 走 SFTP,`grep`/`glob` 在远端跑ripgrep,`bash`/`pwsh` 命令通过 ssh2 exec 执行。本地路径沿用 harness 自身的沙箱。
|
|
18
|
+
- **感知权限策略的远端写操作** —— harness 的文件策略同样作用于远程工作区:`read-only` 会拒绝 `write`/`edit` 和远程 shell 命令,`workspace-write` 把 `write`/`edit` 限制在远程工作区根目录(以及 `/tmp`)内、而远程 shell 命令需经 `danger-full-access` 审批后放行,`danger-full-access` 则交由 SSH 账号自身权限决定。
|
|
19
|
+
|
|
20
|
+
## 工作原理
|
|
21
|
+
|
|
22
|
+
bundle 分为宿主机侧(host)和浏览器侧(browser)两半:
|
|
23
|
+
|
|
24
|
+
- **Host**(`src/index.js`)—— 基于
|
|
25
|
+
[`ssh2`](https://github.com/mscdex/ssh2) 的 SSH/SFTP 传输、加密主机存储、分流文件系统(`ctx.fs`)与 shell(`ctx.shell`)provider、远程感知的`grep`/`glob` 工具,以及通过 Typert 暴露的 `remoteWorkspaces` Remote 命名空间。
|
|
26
|
+
- **Browser**(`src/client.js`)—— “远程工作区”设置页(按主机分组)以及一个“添加工作区”的选择器。
|
|
27
|
+
|
|
28
|
+
打开远程目录会创建一个**锚点(anchor)**:一个被 harness 接纳为工作区身份的真实但为空的本地目录,外加一个记录其远程来源的元数据文件。注册表(`anchors.json`)把该锚点映射到远程主机与路径,所有文件/命令操作都据此查表路由到远端:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
$DSH_HOME/remote-workspaces/
|
|
32
|
+
├── machines.json # 主机注册表(凭据加密落盘)
|
|
33
|
+
├── anchors.json # 锚点 → 远程来源 路由注册表
|
|
34
|
+
└── <host>-<user>-<port>/ # 每台主机的锚点
|
|
35
|
+
└── <encoded-path>/ # 每个远程路径对应一个空锚点
|
|
36
|
+
│ └── .dsh-remote-meta.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
远程完整路径会被编码进锚点目录名(路径分隔符和 Windows 非法字符变成 `--`),因此 `/home/test` 和 `/data/test` 会落到不同的目录(`home--test` vs `data--test`)。
|
|
40
|
+
|
|
41
|
+
## 环境要求
|
|
42
|
+
|
|
43
|
+
- 一个 DeepSeek Harness 安装(插件在运行时解析其宿主服务)。
|
|
44
|
+
- Node.js ≥ 18(harness 本身运行在 Node 22+)。
|
|
45
|
+
- `ssh2` 是唯一的传输依赖;不需要外部 `ssh` 命令。
|
|
46
|
+
- 远端主机需要 `rg`(ripgrep)以支持 `grep`/`glob`,以及 `sha256sum`(或`shasum`)用于写入后校验——二者缺失时都会优雅降级。
|
|
47
|
+
|
|
48
|
+
## 安装
|
|
49
|
+
|
|
50
|
+
从本仓库在线安装:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
dsh plugin --profile web add github:januory/dsh-remote-workspaces
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
发布到 npm 后,也可以直接安装已发布的包:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
dsh plugin --profile web add dsh-remote-workspaces
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
从源码安装:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
git clone https://github.com/januory/dsh-remote-workspaces.git
|
|
66
|
+
|
|
67
|
+
cd dsh-remote-workspaces
|
|
68
|
+
|
|
69
|
+
pnpm install # 安装 ssh2 传输依赖
|
|
70
|
+
|
|
71
|
+
dsh plugin --profile web add .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
卸载:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
dsh plugin --profile web remove dsh-remote-workspaces
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> 维护者注:npm 包名现为 `dsh-remote-workspaces`(早期 git 安装时的包名是 `remote-workspaces`);旧安装请先 `dsh plugin --profile <name> remove remote-workspaces`,再按新名安装。
|
|
81
|
+
|
|
82
|
+
## 使用
|
|
83
|
+
|
|
84
|
+
1. 打开 **设置 → 远程工作区**。
|
|
85
|
+
2. 添加一台 SSH 主机(或从 `~/.ssh/config` 导入),然后点 **测试连接**。
|
|
86
|
+
3. 在“添加工作区”流程里选择远程主机、浏览到某个目录并打开。harness 会把空锚点接纳为工作区,之后的读取、写入、搜索和 shell 命令都在远端执行。
|
|
87
|
+
|
|
88
|
+
## Remote API
|
|
89
|
+
|
|
90
|
+
宿主通过 Typert 暴露 `remoteWorkspaces` Remote 命名空间,包含这些调用:
|
|
91
|
+
`listMachines`、`saveMachine`、`deleteMachine`、`listSshAliases`、`sshAliasDetail`、`testConnection`、`listRemoteDir`、`openRemoteWorkspace`。
|
|
92
|
+
|
|
93
|
+
## 目录结构
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
src/ # DSH bundle 源码
|
|
97
|
+
index.js # 宿主入口(分流 fs/shell、搜索工具、Remote 命名空间)
|
|
98
|
+
client.js # 浏览器入口(设置页 UI + 选择器)
|
|
99
|
+
transport.js # ssh2 传输(SshClient、exec、sha256)
|
|
100
|
+
routing-fs.js # 分流文件系统(远端 SFTP / 本地围栏)
|
|
101
|
+
fs-sftp.js # SFTP 文件系统后端
|
|
102
|
+
local-backend.js # 本地文件系统后端
|
|
103
|
+
containment.js # 本地沙箱围栏
|
|
104
|
+
shell-exec.js # SshShellExecutor(远端 ssh2 exec / 本地 subprocess)
|
|
105
|
+
search.js # 远程感知的 grep/glob 工具
|
|
106
|
+
anchor.js # 本地锚点布局(空目录 + 元数据)
|
|
107
|
+
registry.js # anchors.json 路由注册表
|
|
108
|
+
machine-store.js # 主机注册表(加密落盘)
|
|
109
|
+
ssh-config.js # ~/.ssh/config 解析
|
|
110
|
+
ssh-uri.js # ssh:// URI 解析与识别
|
|
111
|
+
errors.js # 错误码
|
|
112
|
+
cordis.patch.yml # DSH bundle patch(换入分流 provider)
|
|
113
|
+
package.json # 包 + dsh 清单
|
|
114
|
+
test/ # 单元 + 集成测试
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 测试
|
|
118
|
+
|
|
119
|
+
纯单元测试(无需任何主机)可在任意环境运行:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
pnpm test
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
集成测试需要访问真实 SSH 主机,且需先配置好主机注册表(默认使用注册表里的第一台主机,或别名为 `test` 的那台):
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
pnpm test:integration
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 发布(维护者)
|
|
132
|
+
|
|
133
|
+
发布完全手动触发、**零输入**:版本号直接取自 `package.json`,发布前先在 `main` 上提交改好的 `version`,然后 GitHub Actions → **release** → **Run workflow** 即可。工作流会自动执行:跑单元测试(`npm test`)→ 校验 `v<version>` tag 尚未存在(防止重复发布)→ `npm publish`(带 provenance)→ 打 `v<version>` tag 并推送 → 用 `--generate-notes` 创建 GitHub Release。
|
|
134
|
+
|
|
135
|
+
前提:在仓库 **Settings → Secrets and variables → Actions** 中配置名为 `NPM_TOKEN` 的 secret(npm automation token,或对该包有 publish 权限的 granular token);发布账号须是持有该包名的 npm 用户。
|
|
136
|
+
|
|
137
|
+
等价的手工流程:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
npm login
|
|
141
|
+
|
|
142
|
+
npm publish --provenance
|
|
143
|
+
|
|
144
|
+
git tag "v$(node -p \"require('./package.json').version\")"
|
|
145
|
+
|
|
146
|
+
git push origin --tags
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 开源协议
|
|
150
|
+
|
|
151
|
+
[MIT](./LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SSH remote workspace bundle layer.
|
|
2
|
+
#
|
|
3
|
+
# the host half (mounted at the bare package name) provides the routing
|
|
4
|
+
# `ctx.fs` and `ctx.shell` in addition to `ctx.sshClient` and the
|
|
5
|
+
# `remoteWorkspaces` Remote namespace. The base sandbox providers are disabled
|
|
6
|
+
# here so the plugin's routing providers (local half sandboxed via
|
|
7
|
+
# `ctx.sandboxPolicy`/`ctx.sandbox`/`ctx.subprocess`, remote half SFTP/ssh2)
|
|
8
|
+
# register without a duplicate-service throw.
|
|
9
|
+
|
|
10
|
+
- id: fs-sandbox
|
|
11
|
+
name: '@deepseek-ai/dsh-fs-sandbox'
|
|
12
|
+
disabled: true
|
|
13
|
+
- id: bash-sandbox
|
|
14
|
+
name: '@deepseek-ai/dsh-bash-sandbox'
|
|
15
|
+
disabled: true
|
|
16
|
+
- id: pwsh-sandbox
|
|
17
|
+
name: '@deepseek-ai/dsh-pwsh-sandbox'
|
|
18
|
+
disabled: true
|
|
19
|
+
- id: tool-fs-search
|
|
20
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
21
|
+
disabled: true
|
|
22
|
+
|
|
23
|
+
- insert:
|
|
24
|
+
- id: dsh-remote-workspaces
|
|
25
|
+
name: 'dsh-remote-workspaces'
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-remote-workspaces",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Open folders on remote hosts over SSH as DeepSeek Harness workspaces — a non-invasive DSH bundle.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/januory/dsh-remote-workspaces.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"main": "./src/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.js",
|
|
17
|
+
"./client": "./src/client.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"cordis.patch.yml"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node test/unit-anchor-path.mjs && node test/unit-secrets-encryption.mjs && node test/unit-fs.mjs && node test/unit-shell.mjs && node test/unit-routing.mjs && node test/unit-remote-policy.mjs && node test/unit-search-registration.mjs",
|
|
25
|
+
"test:integration": "node test/integration-remote.mjs"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ssh2": "^1.17.0"
|
|
29
|
+
},
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"inject": [
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
37
|
+
"@deepseek-ai/dsh-client-locale",
|
|
38
|
+
"@deepseek-ai/dsh-client-connection",
|
|
39
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
40
|
+
],
|
|
41
|
+
"platform": "web"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/anchor.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local anchor of a remote workspace — the "open a remote folder as a DSH
|
|
3
|
+
* workspace" half.
|
|
4
|
+
*
|
|
5
|
+
* An anchor is a plain EMPTY local directory under
|
|
6
|
+
* `<dsh-home>/remote-workspaces/<host>-<user>-<port>/<encoded-path>` plus a
|
|
7
|
+
* `.dsh-remote-meta.json` describing its remote origin. Because it is a real
|
|
8
|
+
* local directory, `fs.realpath` succeeds and the harness adopts it through
|
|
9
|
+
* `ctx.workspaceRegistry.create` with zero core modification — but it holds NO
|
|
10
|
+
* file content: all file and command I/O is routed straight to the remote over
|
|
11
|
+
* SSH (see registry.js / routing-fs.js / shell-exec.js).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { machinesRoot } from './machine-store.js'
|
|
17
|
+
|
|
18
|
+
/** Root holding every remote host's anchors (alongside machines.json). */
|
|
19
|
+
export function remoteWorkspacesRoot() {
|
|
20
|
+
return join(machinesRoot(), 'remote-workspaces')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Per-machine anchor root: `<dsh-home>/remote-workspaces/<host>-<user>-<port>`. */
|
|
24
|
+
export function anchorRootFor(machine) {
|
|
25
|
+
const m = machine ?? {}
|
|
26
|
+
const tag = [m.host, m.user, m.port]
|
|
27
|
+
.filter((x) => x !== null && x !== undefined && x !== '')
|
|
28
|
+
.join('-')
|
|
29
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_') || 'host'
|
|
30
|
+
return join(remoteWorkspacesRoot(), tag)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeBase(name) {
|
|
34
|
+
const base = String(name || '')
|
|
35
|
+
.replace(/[\\/:*?"<>|]+/g, '--') // path separators + Windows-illegal chars → '--'
|
|
36
|
+
.replace(/^--+|--+$/g, '') // trim leading/trailing separators
|
|
37
|
+
return base === '' || base === '.' || base === '..' ? 'workspace' : base
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Local anchor dir for a specific remote path (idempotent). The FULL remote
|
|
42
|
+
* path is encoded (not just its basename), so two different directories that
|
|
43
|
+
* share a basename — e.g. `/home/test` vs `/data/test` — land in distinct
|
|
44
|
+
* local dirs (`home--test` vs `data--test`) instead of colliding on `test`.
|
|
45
|
+
*/
|
|
46
|
+
export function anchorDirFor(machine, remotePath) {
|
|
47
|
+
const rel = String(remotePath || '').replace(/^\/+/, '')
|
|
48
|
+
return join(anchorRootFor(machine), safeBase(rel))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Create the anchor dir and stamp its remote-origin metadata. */
|
|
52
|
+
export function ensureAnchor(machine, remotePath) {
|
|
53
|
+
const dir = anchorDirFor(machine, remotePath)
|
|
54
|
+
mkdirSync(dir, { recursive: true })
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(dir, '.dsh-remote-meta.json'),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
host: machine?.host ?? null,
|
|
59
|
+
port: machine?.port ?? null,
|
|
60
|
+
username: machine?.user ?? null,
|
|
61
|
+
remotePath,
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
}, null, 2) + '\n',
|
|
64
|
+
'utf8',
|
|
65
|
+
)
|
|
66
|
+
return dir
|
|
67
|
+
}
|