Models & Providers
dsh-ssh-bridge
lance-kanglu/dsh-ssh-bridge · ★ 0
Local SSH bridge plugin for DeepSeek Harness (DSH) — password via browser page, agent exec via localhost API, OpenWrt router management
Manifest details
安装
dsh plugin --profile web add github:lance-kanglu/dsh-ssh-bridgeGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
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, sodsh plugin addexits non-zero and the plugin is not mounted even though the dependency is installed. Allow them in the profile'spnpm-workspace.yamlfirst, 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
- Open
http://127.0.0.1:23991, enter the target host (user@host[:port]) and the password, then click "Connect" or press Enter; - 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;
- Typing
exitin the terminal ends the whole SSH session (same as a regular SSH client); the "Disconnect" button works as well; - After disconnecting, the target host is kept and the password is cleared, so you can reconnect with the password;
- 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.1only, unreachable from the outside network; the password flows only between the browser → local process → encrypted SSH channel. Note: if port23991is 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 theConfigfield,schemais ignored;inject: ['tools']declares the hard dependency, otherwise property access likectx.toolsthrowscannot get property ... without inject;- Registers executors via
tools.register()only (the definition must includeoutput: { schema, render }, otherwiseregisterthrows a TypeError). Tool schemas are injected into the prompt automatically by DSH'sToolRuntime.wireSchemasfrom the registry — do not also callsystemPrompt.tools(), or the model request fails withTool names must be unique(INVALID_REQUEST 400); - Cleanup is registered through
ctx.effect()— cordis invokes a function-declarationapplywithnew, 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 addfails withERR_PNPM_IGNORED_BUILDS: pnpm 11 blocks the native build scripts ofssh2/cpu-featuresby default, soaddexits non-zero and the plugin is not mounted. ConfigureallowBuilds: { cpu-features: true, ssh2: true }in the profile'spnpm-workspace.yamland 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;34mappear in the terminal output: stale cache — hard-refresh (an ANSI renderer is built in). /api/execreturnsnot connected: authenticate in the page first, or ask the agent to runrouter_reconnect.- The page still shows connected after typing
exit: old-version behavior; since v0.4exitends the whole session — hard-refresh (Ctrl+F5) if the page cached the old script. /api/authreturnsmissing target: the request body must carrytargetinuser@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 addinstall, and the standalone run moved to an optional subsection; - Documents that pnpm 11 requires allowing the
ssh2/cpu-featuresnative builds inpnpm-workspace.yaml.
v0.4.1
Release:
- Declares a
dsh.bundle.patchmanifest, sodsh plugin add dsh-ssh-bridgeinstalls it in one step; - README documents the npm / GitHub / local tgz install methods.
v0.4
Connection management:
- The page gained a "Disconnect" button; typing
exitin the terminal now ends the whole SSH session (like a regular SSH client), no longer stuck at "connected without a shell"; - Added
/api/disconnectand/api/reconnect, plus the agent-siderouter_disconnectandrouter_reconnecttools, 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-closeevents 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/authgained atargetparameter; the plugin config droppedrouterHost/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 callingsystemPrompt.tools()— registry tools are injected into the prompt byToolRuntime.wireSchemas, and duplication made the model request fail withTool names must be unique; - Added the two plugin contract tests
test-plugin.cjsandtest-mount.cjs.
v0.2
Added:
- The DSH plugin adapter
plugin/index.cjs, registering therouter_exectool (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).