Bundle
dsh-ssh-plugin
SSH tool plugin for DeepSeek Harness: persisted SSH connections, workspace-bound defaults, and the ssh_exec model tool
- Source
- YOLO-LZL
- License
- MIT
- Updated
- Updated 7 days ago
Readme
# dsh-ssh-plugin
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that adds the `ssh_exec` model tool — run commands on a remote Linux server over the system `ssh` client — plus **persisted SSH connections**: save connections once, bind them to a workspace, and let `ssh_exec` use them by default.
- **Key-based auth only** (`BatchMode=yes`) — no password/passphrase prompts, so a missing or unauthorized key fails fast instead of hanging.
- **No remote-shell quoting layer** — the remote command travels through ssh stdin to `bash -s`.
- **Session sandbox aware** — runs the client under the calling session's standing policy; `danger-full-access` runs it unconfined.
- **Recognizes your local `~/.ssh/config`** — hosts defined there need no manual `ssh_connections save`; pass the alias as `ssh_exec` `host` and ssh resolves it, and `ssh_connections list` shows them read-only.
- **Dual-face package** — the host half registers the `ssh-connections` settings namespace and the `ssh_connections` tool; the browser half registers the "SSH Connections" card in **Settings → Plugins**.
- **All harness services except `tools`/`shell` are optional** — profiles without `settings` or `workspaceRegistry` degrade gracefully (see below).
## Install
```sh
dsh plugin --profile <name> add dsh-ssh-plugin # from npm
dsh plugin --profile <name> add github:you/dsh-ssh-plugin # from git
dsh plugin --profile <name> add ./dsh-ssh-plugin # from a local checkout
```
A local checkout must be built first: `pnpm install && pnpm bundle` (git installs build automatically through `prepare`).
Then boot with `dsh --profile <name>`.
The plugin declares the harness packages as peer dependencies; `@deepseek-ai/dsh-base` (the first bundle of every profile) provides them.
## Usage
### ssh_exec
The model gains an `ssh_exec` tool. Ask in natural language (e.g. "ssh to root@10.0.0.5 and show disk usage"), or pass explicit arguments:
| Arg | Required | Notes |
|---|---|---|
| `host` | yes* | Remote host as `[user@]host`, e.g. `root@10.0.0.5`, **or any alias defined in `~/.ssh/config`**. |
| `command` | yes | Bash command(s) run on the remote host via `bash -s`. |
| `port` | no | SSH port. Default 22. |
| `key_path` | no | Absolute path to the private key. Omit for `~/.ssh` defaults and ssh-agent. |
| `timeout_ms` | no | Foreground timeout in ms. Default 30000. |
| `connection` | no | Label of a saved connection; fills in host/port/user/keyPath. |
\* `host` is required in the schema, but at execution the effective host resolves as **explicit `host` → explicit `connection` → workspace default**. Explicit `port`/`key_path` always win over the resolved connection's values. A `host` that is a `~/.ssh/config` alias is handed to ssh unchanged — ssh applies the config's `HostName`/`User`/`Port`/`IdentityFile` itself, so **no `ssh_connections save` is needed for config hosts**.
### ssh_connections
The model also gains an `ssh_connections` tool for durable connection management (persisted through the harness settings store, so it survives restarts):
- `save` — persist a connection (`label`, `host`, optional `port`/`user`/`keyPath`). Labels are unique.
- `list` — show saved connections **plus hosts discovered from the local `~/.ssh/config`** (read-only; pass the alias as `ssh_exec` `host`, no save needed).
- `delete` — remove one by label (also unbinds workspaces that pointed at it).
- `use` — bind the **current workspace** to a connection label so `ssh_exec` may omit `host`; omit `label` to clear the workspace default.
### SSH config discovery
On every call the plugin reads the local OpenSSH client config (`~/.ssh/config`, or `sshConfigPath` when configured) and parses it:
- Concrete `Host` aliases become visible to the model in `ssh_connections list` and are suggested in `ssh_exec` error messages.
- Per ssh semantics, the **first obtained value wins**: global directives and `Host *` blocks provide defaults, later matching blocks cannot override them. `HostName`/`User`/`Port`/`IdentityFile` are resolved; `Match` blocks and `Include` directives are **not** expanded — hosts must live in the file itself.
- Discovery is best-effort: a missing/unreadable config yields an empty list, never an error. Config hosts are never persisted — they exist only in your `~/.ssh/config`.
### Settings card
With a web profile, **Settings → Plugins** shows an "SSH Connections" card styled like the built-in plugin cards (Bash/Terminal, Agent loop, Web search): a collapsible card with an unsaved badge and discard/save actions. Edit the same `connections` list (add/remove rows, save). The card only writes the `connections` field; workspace bindings are managed by `ssh_connections use`.
## Data model
The `ssh-connections` settings namespace (schemastery schema, validated for duplicate labels):
```ts
{
connections: [{
label: string // unique, non-empty
host: string // [user@]host, non-empty
port?: number // 1..65535, default 22
user?: string // optional; prepended to host when host has no user part
keyPath?: string // absolute private-key path; omit for ~/.ssh + ssh-agent
}],
workspaceDefaults: { [workspaceId: string]: string } // workspaceId → label
}
```
Only key **paths** are stored — key content never leaves disk (`role('secret')` is reserved for a future `keyContent` field).
## Configuration
Set row-level defaults in a later patch layer (profile `cordis.patch.yml` or `--patch`):
```yaml
- id: tool-ssh
name: dsh-ssh-plugin
config:
timeoutMs: 30000
connectTimeout: 15
sshConfigPath: C:\Users\me\.ssh\config # optional; defaults to ~/.ssh/config
```
## Degradation matrix
| Composition | Behavior |
|---|---|
| No `ctx.settings` (e.g. TUI) | No namespace, no `ssh_connections` tool; `ssh_exec` works with explicit args only. |
| No `ctx.workspaceRegistry` | `use` and the workspace-default resolution report "workspace registry unavailable"; everything else works. |
| No `~/.ssh/config` (or unreadable) | `ssh_connections list` shows saved connections only; `ssh_exec` suggestions omit config aliases. |
| Namespace not exposed to the browser | The card renders an "unavailable" placeholder. |
## Requirements
- A reachable Linux target with bash and key-based SSH access set up (`~/.ssh/authorized_keys`).
- On Windows, a session whose standing sandbox policy is `danger-full-access`; the restricted token otherwise refuses to start `ssh.exe`.
## Development
```sh
pnpm install
pnpm bundle # tsdown → lib/index.js (host) + lib/client.js (browser bundle)
pnpm types # tsc --emitDeclarationOnly → lib/types/**/*.d.ts
pnpm test # vitest (model + wiring)
```
Install
dsh plugin --profile web add github:YOLO-LZL/dsh-ssh-plugin
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-ssh-plugin from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.