dsh-remote-workspaces
English | 中文
Open folders on remote hosts over SSH as first-class DeepSeek Harness workspaces.
What it does
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.
Features
- 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.
- 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.
~/.ssh/config import — list configured aliases and pre-fill the connection form.
- Connection test — verify a host before using it.
- Transparent tool routing —
read/write/edit go through SFTP, grep/glob run ripgrep on the remote, and bash/pwsh commands execute over ssh2 exec. Routing follows the session, not the path's spelling: in a remote workspace every command runs on that host, and a workdir naming no path there is refused rather than run locally. Local sessions keep the harness's sandbox.
- 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.
How it works
The bundle has a host half and a browser half:
- Host (
src/index.js) — an SSH/SFTP transport built on 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.
- Browser (
src/client.js) — a "Remote Workspaces" settings section (hosts grouped by machine) and a workspace-add picker.
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:
$DSH_HOME/remote-workspaces/
├── machines.json # host registry (credentials encrypted at rest)
├── anchors.json # anchor → remote origin routing registry
└── <host>-<user>-<port>/ # per-host anchors
└── <encoded-path>/ # one empty anchor per remote path
│ └── .dsh-remote-meta.json
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).
Requirements
- A DeepSeek Harness installation (the plugin resolves its host services at runtime).
- Node.js ≥ 18 (the harness itself runs on Node 22+).
ssh2 is the only transport dependency; no external ssh binary is needed.
- The remote host needs
rg (ripgrep) for grep/glob, and sha256sum (or shasum) for post-write verification — both degrade gracefully when absent.
Installation
Install from this repository:
dsh plugin --profile web add github:januory/dsh-remote-workspaces
Once published to npm, install the released package directly:
dsh plugin --profile web add dsh-remote-workspaces
Install from source:
git clone https://github.com/januory/dsh-remote-workspaces.git
cd dsh-remote-workspaces
pnpm install # install the ssh2 transport dependency
dsh plugin --profile web add .
Remove it with:
dsh plugin --profile web remove dsh-remote-workspaces
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.
Usage
- Open Settings → Remote Workspaces.
- Add an SSH host (or import one from
~/.ssh/config), then Test connection.
- 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.
Remote API
The host exposes a remoteWorkspaces Remote namespace (Typert) with these invocations: listMachines, saveMachine, deleteMachine, listSshAliases, sshAliasDetail, testConnection, listRemoteDir, and openRemoteWorkspace.
Repository structure
src/ # the DSH bundle source
index.js # host entry (routing fs/shell, search tools, Remote namespace)
client.js # browser entry (settings UI + picker)
transport.js # ssh2 transport (SshClient, exec, sha256)
routing-fs.js # routing filesystem (remote SFTP / local fence)
fs-sftp.js # SFTP filesystem backend
local-backend.js # local filesystem backend
containment.js # local sandbox containment fence
shell-exec.js # SshShellExecutor (remote ssh2 exec / local subprocess)
search.js # remote-aware grep/glob tools
anchor.js # local anchor layout (empty dir + meta)
registry.js # anchors.json routing registry
machine-store.js # host registry (encrypted at rest)
ssh-config.js # ~/.ssh/config parser
ssh-uri.js # ssh:// URI parsing + detection
errors.js # error codes
cordis.patch.yml # DSH bundle patch (swap in the routing providers)
package.json # package + dsh manifest
test/ # unit + integration tests
Testing
Pure unit tests (no host required) run anywhere:
pnpm test
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):
pnpm test:integration
Publishing (maintainers)
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.
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.
Equivalent manual flow:
npm login
npm publish --provenance
git tag "v$(node -p \"require('./package.json').version\")"
git push origin --tags
License
MIT