Models & Providers

dsh-ssh-bridge

lance-kanglu/dsh-ssh-bridge · ★ 0

lance-kanglu

Local SSH bridge plugin for DeepSeek Harness (DSH) — password via browser page, agent exec via localhost API, OpenWrt router management

License
MIT
Added
2026-08-16
Maintenance
active
Install kind
github
npm
dsh-ssh-bridge
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-08-19
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add github:lance-kanglu/dsh-ssh-bridge

GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: en · Read Chinese README

Table of contents
  1. Deployment
  2. Dependencies
  3. Steps
  4. Standalone run (optional)
  5. Usage
  6. Browser page
  7. Command API (for agents / scripts)
  8. DSH plugin integration
  9. Mechanism
  10. Plugin adapter
  11. Code structure
  12. FAQ
  13. Changelog
  14. v0.4.2
  15. v0.4.1
  16. v0.4
  17. v0.3
  18. v0.2
  19. v0.1

中文

A local SSH bridge service: enter the target host (user@host[:port]) and password in a browser page → local bridge process → encrypted SSH channel → target host, the password never enters the chat history. It is primarily used as a DSH (DeepSeek Harness) plugin: the agent runs remote commands from the local machine through tools like router_exec, against any SSH target with no host preset; the bridge core does not depend on DSH and can also run standalone.

Current version: v0.4.2

Deployment

Dependencies

Program Version Description
DSH 0.1.0-rc series Host (plugin mode)
pnpm — Dependency install (forwarded by dsh plugin)
Node.js ≥18 Runtime
ssh2 ^1.17 SSH client library (installed automatically by npm install)
@deepseek-ai/schemastery ^3.18 Plugin config schema (installed automatically by npm install)
@deepseek-ai/cordis ^4.0 DSH host runtime (peer dependency, provided by the host, optional)

Steps

# 1. Install the plugin (from the npm registry)
dsh plugin --profile web add dsh-ssh-bridge

# 2. Restart DSH
dsh web

Note: pnpm 11 blocks the native build scripts of ssh2/cpu-features (node-gyp) by default, so dsh plugin add exits non-zero and the plugin is not mounted even though the dependency is installed. Allow them in the profile's pnpm-workspace.yaml first, then re-run the install:

allowBuilds:
  cpu-features: true
  ssh2: true

Other install sources (GitHub / local tgz):

# GitHub (tag matches the npm version)
dsh plugin --profile web add github:lance-kanglu/dsh-ssh-bridge#v0.4.2

# Local tgz (downloadable from GitHub Releases)
dsh plugin --profile web add /path/to/dsh-ssh-bridge-0.4.2.tgz

dsh plugin add appends the package to the profile's dsh.profile.bundles list; the plugin mounts automatically after a DSH restart, with no config file edits. Tools are registered in the global layer, visible to every session's agent.

Verify the deployment: the composed tree of dsh --profile web --dump-config should contain:

- id: ssh-bridge
  name: dsh-ssh-bridge/plugin
  config:
    basePort: 23991

Manual approach (local development, bypassing the bundle): place the package under the deployment's profiles/node_modules (a junction pointing to this project works), then add one row to a cordis patch / preset under $DSH_HOME:

- id: ssh-bridge
  name: dsh-ssh-bridge/plugin
  config:
    basePort: 23991

Tip: for presets, agentPresets.standingKeyFor('<preset-id>') mount-validates (it really mounts the whole plugin tree).

Standalone run (optional)

The bridge core does not depend on DSH and can run standalone:

# 1. Install dependencies
npm install

# 2. Start the service
npm start
# equivalent to: node bridge.cjs

# 3. Open the page
# visit http://127.0.0.1:23991 in a browser

Verify the deployment: the startup log prints the line below and the page opens in a browser:

[ssh-bridge] 已启动: http://127.0.0.1:23991

Usage

Browser page

  1. Open http://127.0.0.1:23991, enter the target host (user@host[:port]) and the password, then click "Connect" or press Enter;
  2. Once the status turns to "Connected", the input box below becomes the target's interactive terminal (with ANSI color rendering), and a "Disconnect" button appears in the header;
  3. Typing exit in the terminal ends the whole SSH session (same as a regular SSH client); the "Disconnect" button works as well;
  4. After disconnecting, the target host is kept and the password is cleared, so you can reconnect with the password;
  5. The log panel at the bottom shows connection and request status in real time, with details when something fails.

Command API (for agents / scripts)

A random token is generated on every start and printed in the startup log ([ssh-bridge] token=xxxx); every call below must carry it:

Endpoint Method Params Description
/api/auth POST {password, target?} Submit the target and password to establish an SSH connection (password is never logged or persisted; target format user@host[:port])
/api/exec POST {cmd, token} Run one command on the target host, returns {code, stdout, stderr}
/api/input POST {line, token} Write one line to the interactive terminal (writing exit ends the whole session)
/api/ping POST {token} Query connection status {ok, connected}
/api/disconnect POST {token} Disconnect the SSH connection (same as the "Disconnect" button)
/api/reconnect POST {token} Reconnect using the credentials saved during page authentication; returns 409 if never authenticated

Example (PowerShell):

$body = @{ token = '<token from the startup log>'; cmd = 'uname -a' } | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:23991/api/exec -Method Post -ContentType 'application/json' -Body $body

Security: the service listens on 127.0.0.1 only, unreachable from the outside network; the password flows only between the browser → local process → encrypted SSH channel. Note: if port 23991 is taken, the service rolls forward automatically (23992, 23993...), as printed in the startup log. Tip: after updating the page code, hard-refresh (Ctrl+F5) to avoid stale cache; the web terminal echoes Enter only.

DSH plugin integration

Mechanism

A DSH plugin is a cordis plugin (npm package): the loader (@deepseek-ai/cordis-plugin-loader) imports the plugin by package name and applies its config, with rows declared in cordis.yml or patch files; model tools are surfaced through the tools registry, with schemas auto-injected into the prompt (tool-pwsh, tool-bash use the same mechanism).

Plugin adapter

plugin/index.cjs is a cordis plugin object (calibrated against the actual DSH 0.1.0-rc runtime source):

  • Config (schemastery) declares config with default-value validation — cordis only reads the Config field, schema is ignored;
  • inject: ['tools'] declares the hard dependency, otherwise property access like ctx.tools throws cannot get property ... without inject;
  • Registers executors via tools.register() only (the definition must include output: { schema, render }, otherwise register throws a TypeError). Tool schemas are injected into the prompt automatically by DSH's ToolRuntime.wireSchemas from the registry — do not also call systemPrompt.tools(), or the model request fails with Tool names must be unique (INVALID_REQUEST 400);
  • Cleanup is registered through ctx.effect() — cordis invokes a function-declaration apply with new, so the apply return value is discarded as a class instance and cannot be used for disposal.

Three tools are registered:

Tool Purpose
router_exec Run one command on the target host; the description carries the bridge page URL and explicitly tells the agent not to solicit or repeat login info from the chat
router_disconnect Disconnect the SSH connection (same as the page's "Disconnect")
router_reconnect Reconnect using the credentials saved during page authentication (password stays in process memory only)

Three contract/e2e test suites are included: node test-plugin.cjs (cordis inject/Config/register contract, including the "no duplicate schema registration" regression assertion), node test-mount.cjs (real SystemPrompt/ToolRuntime + scoped ctx end-to-end, asserting the three tools enter the scoped layer, appear exactly once in the prompt assembly, and tool names are globally unique), and node test-e2e.cjs (a real local ssh2 server: auth → exec → disconnect → reconnect → exit ends the session).

Config options
Option Default Description
bindHost 127.0.0.1 Bind address; keep it loopback
basePort 23991 Starting port; rolls forward when taken
execTimeoutMs 60000 Per-command timeout

Note: the target host is not preset; enter user@host[:port] during page authentication. In CLI mode, environment variables can provide a default target that only pre-fills the page.

Note: the plugin ABI is currently in the 0.1.0-rc series with no public third-party docs; this adapter is calibrated against the contract above and tested.

Code structure

File Purpose
server.cjs Bridge core createBridge(), shared by the CLI and the plugin
bridge.cjs CLI entry, supports environment-variable config (SSH_BRIDGE_PORT, SSH_BRIDGE_ROUTER, etc.)
page.js Browser page script (ANSI rendering)
plugin/index.cjs DSH plugin adapter
test-parser.cjs ANSI renderer test (npm test)
test-plugin.cjs Plugin cordis contract test (node test-plugin.cjs)
test-mount.cjs Plugin scoped-mount e2e test (node test-mount.cjs)
test-e2e.cjs Bridge-core e2e test (local ssh2 server, node test-e2e.cjs)

FAQ

  • dsh plugin add fails with ERR_PNPM_IGNORED_BUILDS: pnpm 11 blocks the native build scripts of ssh2/cpu-features by default, so add exits non-zero and the plugin is not mounted. Configure allowBuilds: { cpu-features: true, ssh2: true } in the profile's pnpm-workspace.yaml and retry.
  • The page does not respond / clicking "Connect" does nothing: hard-refresh (Ctrl+F5); make sure the service is running.
  • Raw escape codes like [1;34m appear in the terminal output: stale cache — hard-refresh (an ANSI renderer is built in).
  • /api/exec returns not connected: authenticate in the page first, or ask the agent to run router_reconnect.
  • The page still shows connected after typing exit: old-version behavior; since v0.4 exit ends the whole session — hard-refresh (Ctrl+F5) if the page cached the old script.
  • /api/auth returns missing target: the request body must carry target in user@host[:port] format; in plugin mode with no default target configured, it is required.
  • Port taken: the service rolls forward automatically, per the startup log.
  • Can the password leak?: No. The password lives in process memory only — never printed, never persisted; the token is local-only and prevents cross-site requests.

Changelog

v0.4.2

Docs:

  • README restructured to be plugin-first: the deployment section is now the dsh plugin add install, and the standalone run moved to an optional subsection;
  • Documents that pnpm 11 requires allowing the ssh2/cpu-features native builds in pnpm-workspace.yaml.

v0.4.1

Release:

  • Declares a dsh.bundle.patch manifest, so dsh plugin add dsh-ssh-bridge installs it in one step;
  • README documents the npm / GitHub / local tgz install methods.

v0.4

Connection management:

  • The page gained a "Disconnect" button; typing exit in the terminal now ends the whole SSH session (like a regular SSH client), no longer stuck at "connected without a shell";
  • Added /api/disconnect and /api/reconnect, plus the agent-side router_disconnect and router_reconnect tools, so the agent can disconnect/reconnect on request (reconnect reuses the credentials saved during page authentication; the password stays in process memory only);
  • Tool descriptions carry the bridge page URL and explicitly require the agent not to solicit or repeat login info from the chat, and not to assume a specific host; the page placeholder is now the neutral user@192.168.x.x;
  • Fixed a connection-lifecycle race: after disconnect/reconnect, stale close/shell-close events from the old connection no longer corrupt or kill the new connection;
  • Added test-e2e.cjs (real local ssh2 server end-to-end).

v0.3

Generalization:

  • The target host is no longer preset; it is entered as user@host[:port] in the browser page, so any SSH target works;
  • /api/auth gained a target parameter; the plugin config dropped routerHost/routerUser/routerPort;
  • The plugin adapter was calibrated to the DSH 0.1.0-rc contract: Config/inject/output {schema, render}/ctx.effect();
  • Fixed duplicate tool registration: the plugin registers via tools.register() only, no longer calling systemPrompt.tools() — registry tools are injected into the prompt by ToolRuntime.wireSchemas, and duplication made the model request fail with Tool names must be unique;
  • Added the two plugin contract tests test-plugin.cjs and test-mount.cjs.

v0.2

Added:

  • The DSH plugin adapter plugin/index.cjs, registering the router_exec tool (experimental);
  • Core refactored into server.cjs (createBridge()), shared by the CLI and the plugin;
  • CLI environment-variable config; added LICENSE (MIT).

v0.1

First release. Core capabilities: browser password authentication, interactive terminal (ANSI color rendering), local exec API, standalone tests (npm test).