scientificmaniac/dsh-ssh-vault

dsh-ssh-vault

把 SSH 主机存进 AES-256-GCM 加密保险库,智能体按别名执行命令,口令经 SSH_ASKPASS 交给 ssh 而不进入命令行。仅支持 Windows。

미분류dsh-plugin-desktopdsh-plugins

dsh-ssh-vault

English | 简体中文

SSH for DeepSeek Harness agents: host address, port, account, password and private key live in an encrypted vault, and the conversation only ever uses an alias. The model never sees a password, and a password never reaches a command line or a remote process list.

Overview

Windows only. The vault data key is protected by Windows DPAPI and the SSH_ASKPASS helper is a Windows .cmd wrapper; the plugin declares os: ["win32"] and refuses to start anywhere else rather than storing the key as plaintext.

Three capabilities in one plugin:

  • Model-facing tools — ssh_hosts, ssh_run, ssh_transfer, ssh_probe, so the agent operates remote hosts by alias.
  • Settings UI — add, edit and delete hosts in Settings → SSH Connections, and test reachability. A password entered there travels in an HTTP body, never through the session log.
  • `/hosts` command — probe hosts and run commands from the conversation without going through the model.

Under the hood it drives the local OpenSSH client (ssh / scp). It does not implement the SSH protocol itself.

Install

From npm. The plugin is published as dsh-ssh-vault; the bare name below resolves against the registry:

dsh plugin --profile desktop install dsh-ssh-vault

To pin a release or install offline, fetch the tarball with npm and hand the file to the same command:

npm pack dsh-ssh-vault        # writes dsh-ssh-vault-<version>.tgz into the current directory
dsh plugin --profile desktop install ./dsh-ssh-vault-<version>.tgz

From a local checkout. The argument is a path, so . works when you are already inside the plugin directory:

# inside the checkout
dsh plugin --profile desktop install .

# or point at it from elsewhere
dsh plugin --profile desktop install ../dsh-ssh-vault

# or install from a packed tarball
dsh plugin --profile desktop install ./dsh-ssh-vault-<version>.tgz

`--profile` is required. DSH isolates plugins per profile, and one machine can carry several (desktop, cli, …). Omitting it falls back to whichever profile is currently default, which is an easy way to install into the wrong place. Confirm the landing spot with:

dsh plugin --profile desktop list

Restart DSH after installing. Host-side tools and routes are loaded at startup.

This package is Windows only. package.json declares os: ["win32"], so dsh plugin ... install refuses to install it on Linux or macOS; a checkout loaded on another platform refuses to start for the same reason (see Known limitations).

Requirements

Item · Requirement Node · ≥ 20 Machine · Windows 10 / 11 with the OpenSSH Client enabled (ssh -V prints a version) Platform · Windows only (win32) — installation and loading are refused on Linux / macOS

Quick start

  1. Install, then restart DSH.
  2. Open Settings → SSH Connections, click "Add connection", fill in alias, address, port, account, and choose password or private-key authentication.

Each card carries a health dot that is checked automatically: grey = not checked, yellow = checking, green = reachable, red = unreachable.

  1. Say something in the conversation, for example:
run uptime on kali

The model calls ssh_run and the result appears as a terminal card.

Do not paste passwords into the conversation. A password entered in Settings goes straight into the encrypted vault and never enters the model context.

Usage

Model-facing tools

Tool · Purpose · Typical phrasing ssh_hosts · Manage the vault (list / show / add / update / remove) · "which hosts do I have", "change kali's note" ssh_run · Run a command on a host, return stdout and stderr · "check disk usage on kali" ssh_transfer · Upload or download a file (scp) · "copy this file to /tmp on kali" ssh_probe · Reachability self-check returning system facts · "is kali still reachable"

Where to see the result: calls to ssh_run, ssh_probe and ssh_transfer render as a terminal card — a header with "host · command" and a state dot, above a scrollable monospace output area. The dot is blue while running, grey on success, red on failure (including a non-zero exit code).

Settings UI

Settings → SSH Connections provides:

  • A host list with a health dot per card; hover for detail
  • Add / edit / delete connections (the alias is editable, credentials move with it)
  • A "Test connection" button per card, whose result also drives the dot
  • "Open in file manager" for the vault directory
  • Password and private-key authentication; a key can be uploaded whole and is written into the vault with its permissions tightened

Language

Everything the plugin shows follows the language chosen in Settings → General → Language; there is no separate switch here.

  • The settings page and the in-conversation SSH cards get their copy from the host's locale service, so they change the moment the language does.
  • /hosts output, transport diagnostics, Settings-API errors, vault errors and background-job status are generated on the Host; they read the same persisted preference (locale.preference) and follow later changes, falling back to the request's Accept-Language only when nothing was ever chosen.
  • Model-facing definitions stay English: tool names, descriptions and parameter schemas belong to the model interface, not to the UI.

The /hosts command

Start a line with / in the composer and the command runs without going through the model.

/hosts                    list every host, grouped into online / offline
/hosts kali               probe one
/hosts kali,win10         probe several (either comma form works)
/hosts kali; apt update   run a command on kali
/hosts kali,win10; uptime run on every reachable host
  • Separators accept both ASCII and full-width forms: , and ,, ; and ;
  • If the text after the semicolon reads as natural language (e.g. "check the disk"), the command does not run it and tells you to send that sentence to the conversation instead — command output never reaches the model, so such requests cannot go through the command
  • Output renders as two cards: reachability and command execution, each with its own state dot
  • Picking `/hosts` from the `/` menu only claims it: the composer shows the command token with its hint. Press Enter again to run it bare (list hosts), or type alias; shell command as its arguments. To go back to a normal prompt instead, delete the command token — until then, whatever you type is the command's argument
  • Before the first message the result appears above the composer. DSH treats a transcript containing only command rows as an inactive view, so a bare /hosts in a brand-new conversation used to look like "nothing happened" until you sent a message. The plugin now mirrors the /hosts cards (running state included) in the composer dock while the session is still blank; once the first message exists the transcript takes over, so nothing is duplicated
  • A bare call probes only when the health cache is cold, in parallel and with a 4s budget, so it answers within seconds even when several hosts are unreachable

Architecture

Two halves

This is a Host + Client plugin; the two halves load through different mechanisms:

Half · Entry · Loading Host · index.js · Cordis plugin, inserted into the Loader through cordis.patch.yml Client · client.js · Browser module, declared via dsh.client in package.json and injected into the page by the host

The Host half carries every sensitive operation: vault encryption, driving the local ssh client, web routes, and the registration of commands and tools. The Client half only renders and collects form input; it never executes a command.

Data flow

flowchart TB
    subgraph entry [Entry points]
        chat[Conversation / model tool call]
        settings[Settings form]
        cmd["/hosts command"]
    end

    subgraph host [Host half — index.js]
        resolve[Resolve alias → read credentials → build ssh args]
        vault[(Vault<br/>AES-256-GCM ciphertext)]
        api["Web routes /ssh-vault/*"]
        exec[Spawn local ssh / scp]
    end

    subgraph client [Client half — client.js]
        toolcard[Tool terminal card]
        cmdcard["/hosts double card"]
        form[Connection form]
    end

    proxy[Remote host]

    chat --> resolve
    cmd --> resolve
    settings -->|POST JSON| api
    form -->|same-origin, document-relative| api
    api --> vault
    resolve --> vault
    resolve --> exec
    exec -->|stdout / stderr| toolcard
    exec -->|stdout / stderr| cmdcard
    exec <--> proxy

Three invariants hold across that chain:

  1. A password never reaches a command line — local ssh receives it through SSH_ASKPASS; remote sudo is primed over stdin
  2. Only ciphertext touches disk — AES-256-GCM, with the data key protected by Windows DPAPI (or, optionally, derived from a master passphrase via scrypt)
  3. `~/.ssh` is never touched — known_hosts points into the vault directory and HashKnownHosts is enabled

Configuration

The plugin has no user-configurable options. config: {} in cordis.patch.yml is an empty object, and the Loader entry id is fixed as ssh-vault.

Behaviour is shaped by the following:

Item · Notes Vault directory · Defaults to $DSH_HOME/ssh-vault; DSH_HOME is decided by DSH Key protection · dpapi (Windows DPAPI, the default) or passphrase (scrypt-derived key, no key material on disk). file stores the key as plaintext and is an explicit opt-in only Timeouts · ssh_run defaults to 120s, adjustable, capped at 30 minutes; commands inside /hosts are fixed at 120s Health checks · Settings page and /hosts probe with a 15s timeout; the background sweep refreshes every 5 minutes

API and extension points

Host-side services are reached through ctx.inject as optional dependencies; a missing service does not fail the plugin, it just disables that capability.

Model tools (ctx.tools.register)

Tool · Key parameters ssh_hosts · action required (list / show / add / update / remove); reveal controls whether address and account are masked ssh_run · host, command required; optional sudo, timeoutMs, workdir, run_in_background, wait_for, bindAddress ssh_transfer · host, direction, local, remote required; optional recursive ssh_probe · host required; optional timeoutMs, bindAddress

The three terminal-style tools also register presentCall / presentResult hooks, but the current DSH version has no consumer for them; presentation is handled by the slots below.

Client slots (ctx.slots.register)

Slot · Key · Effect settings.section · id: ssh-vault, order: 20, label · The "SSH Connections" page in the settings panel tool.call.toolview · ssh_run, ssh_probe, ssh_transfer · Renders those tool calls as terminal cards conversation.chat.commandview · hosts · Renders /hosts output as two terminal cards

The tool.call.toolview key space is open; none of these three names were taken, so no built-in UI is shadowed.

All three registrations also carry locale: 'ssh-vault' — the namespace registered with the host's locale service — which is what makes the rendered cards re-render and re-read their copy when the language changes.

Human command (ctx.commands.register)

Command · Notes /hosts · Probe hosts / run a command on them. Form: alias[,alias][; shell command]

The command's description and input.hint are re-registered only when the interface language changes. The disposer returned by commands.register destroys the current definition, so replacing it while the user is typing arguments strips the composer of its command context (backspace stops working, and the UI needs a restart). That is why registration is never driven by timers or health results; a locale switch is a deliberate Settings action with nothing being typed, making it the one safe moment to swap the definition.

HTTP routes (ctx.webServer.register)

Mounted at /ssh-vault (kind: 'prefix', no trailing slash). POST only, JSON in and out, with an envelope of { ok: true, value } or { ok: false, error }.

Path · Effect /ssh-vault/list · Host list (address and account masked, no passwords) /ssh-vault/save · Create / update / rename /ssh-vault/remove · Delete /ssh-vault/test · Reachability probe /ssh-vault/reveal · Return unmasked address and account /ssh-vault/openVault · Open the vault directory in the system file manager

openVault accepts no path argument at all — it can only open the plugin's own vault directory.

Every request crosses the Host Connection trust fence before the handler looks at the method, the body or the vault: it calls ctx.connection.requestRejection(req), the same Host/Origin check and browser-session authentication used by /api. A rejected request gets 401 / 403 and never reaches the vault, so a DNS-rebinding or cross-site call cannot read host data or trigger a probe.

About ctx.systemPrompt.section

This plugin does not use that extension point. Every tool carries its own description, and no section is injected into the system prompt, so prompt length and ordering are unaffected.

Lifecycle

Startup

  1. The Loader reads cordis.patch.yml and inserts the entry with id: ssh-vault
  2. The host imports index.js and calls apply(ctx, config)
  3. apply opens the vault, registers the four tools, and — where the services exist — the web routes, the /hosts command and the background health timer. It also subscribes to the host settings document so it can read the interface language and follow later switches
  4. The Client half is injected into the page per the dsh.client declaration, and its apply registers the three slots and its ssh-vault dictionary with the locale service

Unload and resource reclamation

Async resources are wrapped in ctx.effect, and Cordis runs the returned cleanups in reverse order on teardown:

  • Tool and command disposers (including the locale-switched command definition)
  • Web route disposers
  • The settings-document subscription and its startup retry timers
  • The health sweep's setTimeout / setInterval
  • The vault handle
  • The client's locale dictionary and slot registrations