Skip to content
dsh.fish
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

  • 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.
Source