dsh-ssh
DeepSeek Harness 的 SSH 远程执行插件:ProxyJump 链、SFTP 文件系统、基于 ssh2 的子进程与 PTY。
uynajgi
@uynajgi
⬇ 2
★ 6
main
安装
dsh plugin --profile web add github:uynajgi/dsh-ssh
需要可复现安装时,可在仓库后追加 #commit 固定提交。
DeepSeek Harness 的 SSH 远程执行插件:ProxyJump 链、SFTP 文件系统、基于 ssh2 的子进程与 PTY。
该插件未提供要点说明,请参考仓库 README。
dsh-plugindeepseek-harnesspluginproxyjumpremote-developmentsftp
- 安装并启动 DeepSeek Harness:
npx @deepseek-ai/dsh web - 在终端执行上面的安装命令(CLI 会解析插件并核验来源)
- 用 dsh plugins list 确认已安装,必要时重启 Harness 生效
插件以当前 dsh 进程的权限运行,安装时可能执行代码。请先通读仓库源码与许可证,确认无破坏性命令与越权访问;本站只做索引,不对第三方插件安全性作担保。
| 代码仓库 | github.com/uynajgi/dsh-ssh |
| 许可证 | MIT |
| 主要语言 | main |
| 下载量 | 2 |
| GitHub 星标 | 6 |
| 最近推送 | 2026-08-16 |
| 收录日期 | 2026-09-19 |
| 分类 | 工具与能力 |
事实信息来自公开插件目录快照(2026-10-03),介绍文案由本站再加工。
以下为插件仓库 README 全文(原始内容,由公开目录抓取整理)。
# dsh-ssh
English | [中文](README.zh.md)
[图片: npm version]
[图片: license]
[图片: node version]
[图片: CI status]
[图片: GitHub stars]
[图片: dsh-plugin]
**SSH remote-execution plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).** Moves Bash, file tools, PTY terminals, and LSP onto a remote host over a single SSH connection — with multi-hop ProxyJump chains, SFTP upload/download, and full auth coverage. Built on [ssh2](https://github.com/mscdex/ssh2).
> First (and as of 2026-08, only) SSH remote-development plugin in the dsh-plugin ecosystem. Verified end-to-end against a real two-hop jump environment with key auth.
## Architecture: local brain, remote hands
```
Your machine (deepseek-harness) Remote host
┌────────────────────────────────────┐ SSH ┌──────────────────────┐
│ agent loop (orchestration, memory) │◄──────────►│ bash / command exec │
│ LLM API calls (direct, no egress) │ exec │ filesystem (SFTP) │
│ credentials / config / sessions │ pty │ PTY terminals │
│ ctx.subprocess → dsh-ssh │ sftp │ LSP / git / builds │
│ ctx.fs → dsh-ssh │ │ │
└────────────────────────────────────┘ └──────────────────────┘
```
**The harness does not need to be installed remotely.** dsh-ssh implements remote providers for two of the harness's capability seams — `ctx.subprocess` (remote processes) and `ctx.fs` (remote files). Every tool built on those seams (bash, file tools, terminals, LSP, subagent processes) switches to the remote host with zero changes: the model thinks locally, commands run remotely, results stream back into the local model context.
## Install
```sh
npm i dsh-ssh
```
## Quick start (cordis.yml)
**One row mounts everything** — the shared connection owner plus both remote providers:
```yaml
- id: ssh-remote
name: dsh-ssh
config:
host: server.example.com # target host (required; a ~/.ssh/config alias like prod works too)
port: 22
username: root # required
privateKey: ~/.ssh/id_ed25519 # identity-file path, or PEM content
# password: 'xxx' # password auth (mutually usable with privateKey)
# agent: 'pageant' # Windows Pageant; Unix: SSH_AUTH_SOCK path
cwd: /root/workspace # remote working directory (required, absolute POSIX path)
# --- ProxyJump chain (optional; first hop from local, last hop to target) ---
jump:
- host: bastion.example.com
# port: 22 # defaults to the target's
# username: ubuntu # defaults to the target's
privateKey: ~/.ssh/id_ed25519
# - host: second-hop ...
# --- Connection & security ---
readyTimeout: 45000 # ~ ConnectTimeout (ms, default 45s; relayed links handshake slowly)
keepaliveInterval: 0 # ~ ServerAliveInterval (0 disables)
keepaliveCountMax: 3 # ~ ServerAliveCountMax
strictHostKeyChecking: false # verify the host key when true
knownHosts: # required when strictHostKeyChecking: true
- 'SHA256:xxxxxxxx...'
```
The aggregate row is equivalent to three subpath rows — mount them separately only when a deployment composes providers individually:
```yaml
- id: ssh
name: dsh-ssh/ssh # ctx.ssh connection owner (config above)
- id: subprocess-ssh
name: dsh-ssh/subprocess # ctx.subprocess remote provider
- id: fs-ssh
name: dsh-ssh/fs # ctx.fs remote provider (SFTP)
```
## Add-workspace over SSH (Web GUI)
The Web surface's **Add workspace** flow (the conversation hero picker and the
sidebar workspace browser) is taken over by the dsh-ssh client UI, laid out as a
**connection sidebar beside a directory browser** (VS Code Remote Explorer
style): the sidebar lists `~/.ssh/config` hosts, saved connections, and the
local entry; the right pane browses whichever side is active. Local listing
rides the `ctx.directoryPicker` `browse` capability; connection management and
remote listing ride dsh-ssh's own `/dsh-ssh` RPC channel. While browsing the
local side, the toolbar also offers a **system chooser** button (the
`local.pickNative` endpoint, reusing the host's OS-native folder dialog) — the
directory picked in the popup becomes the workspace directly, no
level-by-level walking required.
### `~/.ssh/config` hosts in the sidebar (`config.hosts`)
The「SSH 配置主机」sidebar section is driven by the `config.hosts` endpoint:
every opening **re-reads** the host machine's `~/.ssh/config` and lists its
**exact Host aliases** (wildcard patterns such as `*.example.com` stay hidden),
each with the resolved `user@host:port`, IdentityFile presence, and ProxyJump
presence:
- Clicking an alias resolves its full config (username, port, identity, jump
chain), **registers it silently, and drops you straight into that host's
directory browser** — no form (VS Code Remote-SSH style). Registered aliases
get an「已添加」badge and simply switch on click.
- An alias without a `User` skips registration and opens a **prefilled form**
(port / identity / jumps already filled); only the username is missing.
- An alias without an `IdentityFile` can be registered, but the connection
fails at auth — the right pane translates `All configured authentication
methods failed` into a readable hint with a「补全认证」button that opens the
prefilled form.
The「新建连接」form is alias-first as well: type a `~/.ssh/config` alias and
blur or paste auto-resolves and prefills (the manual「识别 ssh 配置」button
remains the loud fallback); a successful resolve shows a one-line summary
(alias → user@host:port, identity path, jump chain) inside the form.
A patch layer's `name` is a match guard rather than a replacement, so the Web
bundle's `@deepseek-ai/dsh-host-directory-picker-auto` row must be **disabled
by id** (its dynamically mounted in-app picker disappears with it) and the SSH
backend inserted under its own id. In the Web profile
(`$DSH_HOME/profiles/web/cordis.patch.yml`):
```yaml
# Disable the boot-resolved picker (its dynamic entries go with it).
- id: directory-picker
name: '@deepseek-ai/dsh-host-directory-picker-auto'
disabled: true
- insert:
- id: ssh-remote
name: dsh-ssh
config: { ...same config as the quick start... }
# The local-directory browse backend for the client directoryFlow slots.
- id: directory-picker-ssh
name: dsh-ssh/picker
config:
maxEntries: 1000
# The connection registry + /dsh-ssh RPC (persistence, ~/.ssh/config
# awareness, remote directory browsing).
- id: ssh-web-channel
name: dsh-ssh/web
config:
maxEntries: 1000
```
Picking a remote directory first asks `/dsh-ssh` `session.route` for a LOCAL
placeholder directory (`/dsh-ssh-routes//`,
created host-side) and creates the session with it:
```ts
const { cwd } = await rpc('session.route', { id: connectionId, path: remotePath })
ctx.sessions.create({ cwd })
```
The detour exists because the host's session service `mkdir`s the project
directory through `node:fs` — an `ssh://…` cwd cannot pass that check, while
`mkdir` succeeds silently for an existing directory. `ctx.subprocess` and
`ctx.fs` recognize both the `ssh:///` spelling and the local
placeholder prefix, routing that session's bash / file / terminal operations
onto the registered connection's directory. Remote sessions do not enter the
DSH local workspace registry (see「Known limitations」); deleting a connection
also removes its placeholder tree.
### Picker configuration (`dsh-ssh/picker`)
| Field | Type | Default | Description |
|---|---|---|---|
| `maxEntries` | number | 1000 | Complete-result bound for one listed level (hidden rows count; `truncated` flags a cut) |
| `remoteLabel` | string | — | Retained field: the client flow no longer uses a pinned entry; remote entries live in the left connection sidebar |
`dsh-ssh/picker` now serves only the `ctx.directoryPicker` `browse` backend
(local directories keep working on Windows hosts; POSIX absolute paths go to
the aggregate SSH connection). The client UI's remote connection list and
remote directory browsing ride the `dsh-ssh/web` RPC channel instead.
## Configuration reference (`dsh-ssh/ssh`)
| Field | Type | Default | Description |
|---|---|---|---|
| `host` | string | — | Target hostname or address (required) |
| `port` | number | 22 | Target SSH port |
| `username` | string | — | Remote login user (required) |
| `password` | string | — | Password auth |
| `privateKey` | string | — | PEM key content or local identity-file path |
| `passphrase` | string | — | Passphrase for an encrypted key |
| `agent` | string | — | ssh-agent socket path or `pageant` |
| `jump` | JumpConfig[] | `[]` | ProxyJump chain; per-hop port/user/auth overrides |
| `cwd` | string | — | Remote working directory (required, absolute POSIX path) |
| `readyTimeout` | number | 45000 | Connection timeout (ms) |
| `keepaliveInterval` | number | 0 | SSH keepalive interval (ms) |
| `keepaliveCountMax` | number | 3 | Keepalive failure threshold |
| `strictHostKeyChecking` | boolean | false | Verify the host key against `knownHosts` |
| `knownHosts` | string[] | `[]` | Trusted fingerprints (`SHA256:…`) or raw base64 public keys |
### OpenSSH `~/.ssh/config` mapping
| OpenSSH directive | dsh-ssh field |
|---|---|
| `HostName` / `Port` / `User` | `host` / `port` / `username` |
| `IdentityFile` / `IdentitiesOnly` | `privateKey` (path or PEM) |
| `PasswordAuthentication` | `password` |
| `ForwardAgent` | `agent` |
| `ProxyJump` (comma-separated hops) | `jump` array (per-hop) |
| `ConnectTimeout` | `readyTimeout` |
| `ServerAliveInterval` / `ServerAliveCountMax` | `keepaliveInterval` / `keepaliveCountMax` |
| `StrictHostKeyChecking` + `UserKnownHostsFile` | `strictHostKeyChecking` + `knownHosts` |
| `RemoteCommand` / `RequestTTY` | see `spawnTerminal` (PTY is consumer-requested) |
## Capabilities
| Capability | Implementation |
|---|---|
| ProxyJump chains | `jump` array, multi-hop (direct-tcpip, equivalent to OpenSSH `ProxyJump`), independent auth per hop |
| Auth | password, private key (PEM or path), passphrase, ssh-agent / Pageant; with none configured, falls back to the `~/.ssh` default identities (id_ed25519 / id_ecdsa / id_rsa, mirroring OpenSSH) |
| Upload (local → remote) | SFTP atomic write (same-dir temp file + rename, mode preserved) |
| Download (remote → local) | full fs provider: read / streamText (streaming decode) / readBytes (bounded) / listDir / stat / lstat |
| Remote commands | subprocess provider: collect (bounded tail + local spill file), pipe, inherit, batch stdin |
| Interactive terminals | PTY (`spawnTerminal`), I/O plus TERM→KILL cleanup |
| Add-workspace GUI | `dsh-ssh/picker`: the directory-picker seam's `browse` backend; the client UI is a connection sidebar (`~/.ssh/config` one-click hosts + saved connections + local) beside the directory browser |
| Environment isolation | remote login env scrubbed (`DSH_*` and credential-shaped names removed) + explicit overrides, launched via `env -i` |
| Concurrency safety | fs writes serialized per target key (no interleaved writes) |
| Host verification | `strictHostKeyChecking` + `knownHosts` (SHA256 fingerprints or raw keys) |
## Performance
- **Connection reuse** — all three providers share one SSH connection (jump chain included); the SFTP channel opens lazily, is reused, and rebuilds itself after disconnects.
- **Environment cache** — the remote login environment is read once per connection (`env -0`), not per spawn.
- **Local spill** — collect-mode output keeps an in-memory tail plus a local spill file, same semantics as the official local provider.
- **No polling** — one exec channel per command (`cd && exec env -i -- …`); no polling or intermediate state files.
## Reliability
- **Exit facts are authoritative** — exit code / signal come from the SSH channel close event.
- **UTF-8 safe** — exec output is buffered and decoded once; SSH chunking cannot corrupt multi-byte characters.
- **Fail loud** — connection, auth, jump, and SFTP failures surface with readable messages.
- **Teardown** — plugin disposal terminates active processes/terminals and closes the connection; staging dirs and spill files are cleaned on failure.
## Troubleshooting
| Symptom | Cause & fix |
|---|---|
| `All configured authentication methods failed` | Wrong auth config: check username / privateKey path / passphrase; key permissions too open (`chmod 600`) |
| `Cannot read private key` | `privateKey` is neither PEM content nor an existing file |
| Jump connection timeout | Check hop reachability and `readyTimeout`; verify the hop's user/auth independently |
| `Host key verification failed` | `strictHostKeyChecking: true` without a matching `knownHosts` entry; collect the fingerprint with `ssh-keyscan` |
| exec exits 127 | Remote command not found; check the remote PATH (the scrubbed env keeps it) |
| Write fails with `FS_NOT_OBSERVED` | File exists and `createIfAbsent` was used (overwrite protection, not a bug) |
## Known limitations
- **Remote pid invisible** — SSH channels do not expose the remote pid; `SubprocessHandle.pid` is always `-1`.
- **Termination is not tree-scoped** — `terminate` signals the remote direct process (SIGTERM → grace → SIGKILL); descendants are not guaranteed to die (inherent to the SSH protocol, unlike the local provider's process groups).
- **No foreground process group** — `inspectForeground` returns `undefined` and `signalForeground` throws (the SSH channel cannot resolve a remote foreground group).
- **No reconnection** — a dropped connection requires a plugin restart.
- **Remote directories land as sessions** — picking a remote directory opens a session through a `session.route` local placeholder (`/dsh-ssh-routes//`; the session list shows that cwd); no record is created in the DSH local workspace registry (`dsh-workspace` still only accepts local `fs.realpath` directories).
- **Picker is remote-only on POSIX hosts** — every absolute path is a remote path there, so the local filesystem cannot share the picker's vocabulary (Windows hosts keep both worlds via drive/UNC routing).
- **Text-only streaming** — `streamText` rejects binary files with `FS_NOT_TEXT` (same as the official provider).
## Development
```sh
npm i
npm run typecheck
npm run build # emits lib/ — the compiled payload the harness loader imports
```
- **Git hooks** (husky): `pre-commit` typechecks; `commit-msg` enforces [Conventional Commits](https://www.conventionalcommits.org/); `pre-push` rejects a version tag that does not match `package.json`.
- **CI** (GitHub Actions): typecheck + publishable-payload check on every push/PR.
- **Release** (GitHub Actions): push a version tag to publish to npm and draft a GitHub Release:
```sh
npm version patch -m "chore(release): v%s" # bumps package.json + commits + tags
git push origin main && git push origin --tags
```
The tag must match the `version` field in `package.json` (both hooks and the release workflow enforce it). Publishing uses the `NPM_TOKEN` repository secret (an npm **Automation token** — it bypasses 2FA for CI).
## License
MIT
数据来源:公开的 DeepSeek Harness 插件目录与各插件 GitHub 仓库。本站为独立第三方目录,与 DeepSeek、幻方(High-Flyer)及插件作者均无隶属或背书关系。