Back to home

ZhimingYe

dsh-hub

Remote Development over Reverse Tunnels for DeepSeek Harness (Unix-like Systems Only)

Stars
0
Language
TypeScript
Created
Aug 14, 2026
Updated
Aug 15, 2026

Introduction

DSH Hub

English | 中文

Use DeepSeek Harness in a browser from a Unix-like machine (Linux or macOS). Hub runs on a public machine; the Unix-like machine only makes outbound connections and does not open inbound ports.

browser  ──HTTPS──▶  Hub (public, TLS reverse proxy)  ◀──WSS (outbound)──  Agent + dsh (Unix-like)

Hub login page with Username, Password, and English | 中文

Web workstation after sign-in: workspaces, chat, and sign-out

Both machines need Node.js >= 22.19 and this repository. The Unix-like machine must also be able to run dsh.

node -v

After install the command is dsh-hub; you do not need npx.

1. Install

Run this on both machines:

git clone git@github.com:ZhimingYe/dsh-hub.git
cd dsh-hub
npm install
npm install -g .

Confirm the command is on PATH:

dsh-hub --help

After later upgrades, run npm install -g . again. Without a global install, use npx dsh-hub from the repository root.

On the Unix-like machine, confirm dsh works:

dsh --help

If there is no global dsh, install DeepSeek Harness, run pnpm install && pnpm run build at its repository root, and put dsh on PATH.

2. Start Hub

On the public machine:

dsh-hub serve --user alice

Enter a password at the prompt (it is not echoed). The first run writes hub.yaml in the current directory (mode 0600, bcrypt hashes, not plaintext) and prints once:

wrote /home/you/dsh-hub/hub.yaml
DSH_HUB_AGENT_SECRET=<random>
config: /home/you/dsh-hub/hub.yaml
listen: http://127.0.0.1:8787
connect: dsh-hub connect http://<host>:8787 --user alice

Copy the DSH_HUB_AGENT_SECRET= line to the Unix-like machine immediately (environment variable, --agent-secret-file, or a later prompt). Later serve runs do not print the plaintext. The yaml cannot recover that value.

Allow 443 on the reverse proxy. Open https://<hub-host> in a browser; you should see the login page (English by default, with an English | 中文 switch).

Cleartext HTTP on a non-loopback address is refused by default. Set allowPlainHttp: true and host: 0.0.0.0 only for a short lab trial.

hub.yaml is for Hub only. connect on the Unix-like machine does not read it. The default path is $PWD/hub.yaml; every serve prints the absolute config: path. To choose a file:

dsh-hub serve --config /etc/dsh-hub/hub.yaml
dsh-hub serve --port 8788
host: 127.0.0.1
port: 8787
agentSecret: "$2a$12$...."
users:
  alice: "$2a$12$...."
  bob: "$2a$12$...."
FieldDefaultMeaning
port8787Listen port
host127.0.0.1Listen address; use a reverse proxy on the public side, do not bind Hub to 0.0.0.0
agentSecretrequiredbcrypt hash of the /agent Bearer secret; connect on the Unix-like machine presents the matching plaintext
allowPlainHttpfalseNon-loopback cleartext HTTP is allowed only when true
trustedProxies[]Reverse-proxy IPs allowed to supply X-Forwarded-For / X-Forwarded-Proto; an empty list ignores those headers
usersrequiredLogin usernames to bcrypt hashes

Restart serve after editing the config. Do not put plaintext passwords or secrets in the yaml; a value that is not a bcrypt hash fails at load. To add a user:

dsh-hub hash
dsh-hub hash --password-file ~/.dsh-hub-password

Paste the printed hash into users:. To rotate the agent secret, hash again, replace agentSecret, and update the plaintext on the Unix-like machine. hash reads plaintext only from a prompt or --password-file, not from DSH_HUB_PASSWORD.

3. Connect from a Unix-like machine

On the machine that runs dsh, as the same uid:

dsh-hub connect https://hub.example.com --user alice

Enter the password and agent secret at the prompts. The secret is the DSH_HUB_AGENT_SECRET printed on first serve, not the hash in hub.yaml. The process stays running. After the terminal shows connecting, sign in in the browser with the same account; you should reach the workstation.

dsh HTTP uses a Unix socket: $XDG_RUNTIME_DIR/dsh-hub-<uid>/workstation.sock; if XDG_RUNTIME_DIR is unset, /tmp/dsh-hub-<uid>/workstation.sock. That directory must be owned by this uid and mode 0700 (not a symlink, not owned by someone else), or connect refuses to start. Shared login nodes should set XDG_RUNTIME_DIR.

Reconnect is automatic after a drop.

Non-interactive (file mode must be 0600):

printf '%s' 'your-password' > ~/.dsh-hub-password
printf '%s' 'DSH_HUB_AGENT_SECRET-from-first-serve' > ~/.dsh-hub-agent-secret
chmod 600 ~/.dsh-hub-password ~/.dsh-hub-agent-secret
dsh-hub connect https://hub.example.com --user alice --password-file ~/.dsh-hub-password --agent-secret-file ~/.dsh-hub-agent-secret

Or:

export DSH_HUB_PASSWORD
export DSH_HUB_AGENT_SECRET
dsh-hub connect https://hub.example.com --user alice

The command line does not accept --password or --agent-secret.

When the Unix-like machine needs an HTTP proxy to reach Hub:

export HTTPS_PROXY=http://proxy:port
export HTTP_PROXY=http://proxy:port
dsh-hub connect https://hub.example.com --user alice

connect starts dsh. The default is equivalent to:

dsh --profile workstation --patch <dsh-hub-install>/workstation.cordis.yml

Remaining arguments are forwarded to dsh as-is:

dsh-hub connect https://hub.example.com --user alice --patch extra.yml
dsh-hub connect https://hub.example.com --user alice --profile myweb
dsh-hub connect https://hub.example.com --user alice -- --trusted-host lab.example.com
dsh-hub connect https://hub.example.com --user alice --dump-config

Arguments after -- all go to dsh. Do not disable or replace webserver-unix, hub-logout, or hub-preview in the overlay, or the tunnel or preview will break. --profile myweb still applies the Hub overlay; if that profile is not a Web workstation, the browser stays offline.

Where workstation lives

workstation is a dsh profile for this machine and this user. It is not in the dsh-hub repository. Directory:

$DSH_HOME/profiles/workstation/

If DSH_HOME is unset, that is ~/.dsh/profiles/workstation/. Each user on the machine has their own copy. The first connect creates:

package.json            # dependencies, and dsh.profile.bundles (which bundles load at start)
cordis.patch.yml        # this profile's own patch (starts as [])
pnpm-workspace.yaml
node_modules/

Startup stacks from an empty config; a later layer can change an earlier one:

  1. Bundles in dsh.profile.bundles, in list order. The default is @deepseek-ai/dsh-base and @deepseek-ai/dsh-web-app; bundles installed with dsh plugin add are here too.
  2. This profile's cordis.patch.yml
  3. $DSH_HOME/cordis.patch.yml (shared by every profile on this machine; optional)
  4. Hub's workstation.cordis.yml (added on every connect: Unix socket, sign-out, artifact preview)
  5. Extra --patch files on the command line, in the order written

Hub's @dsh-hub/webserver-unix, @dsh-hub/logout, and @dsh-hub/preview are layer 4. They are not written into dsh.profile.bundles. connect links them into a node_modules that dsh can resolve.

What a later connect overwrites

It does not empty the profile, remove bundles already added with plugin add, or rewrite a cordis.patch.yml you edited.

PathLater connect
cordis.patch.ymlWritten as [] only if the file is missing
pnpm-workspace.yamlWritten only if missing
dsh.profile.bundlesWritten as dsh-base + dsh-web-app only if missing or an empty array
Other dependenciesKept
file: paths for @dsh-hub/webserver-unix, logout, previewRewritten each time to this dsh-hub install
Links for those three packages in node_modulesRelinked each time
npm install in the profile directoryFailures print to stderr only; the profile is not rolled back

dsh plugin uses pnpm in that directory; connect may also run npm install. Do not mix hand npm add and dsh plugin in the same profile.

Add plugins

For lasting use: the package must declare dsh.bundle, and pnpm must be on PATH.

dsh plugin --profile workstation add @some-bundle

A package without dsh.bundle only enters dependencies and does not load at start. To change this workstation's config lines, edit ~/.dsh/profiles/workstation/cordis.patch.yml.

For this session only:

dsh-hub connect https://hub.example.com --user alice --patch /absolute/path/extra.yml

Plugin name values in extra.yml must be absolute paths, not bare package names:

- insert:
  - id: hello
    name: '/ifs1/User/you/scratch-plugin/src/my-plugin.ts'

Install packages with dsh plugin and open the tunnel with dsh-hub connect. Do not put both on one command line (dsh-hub connect … plugin … only runs dsh plugin and then exits, with no tunnel).

Upgrade

What to updateHow
Hub (login, tunnel, preview, sign-out)Update dsh-hub, npm install -g ., then connect
Models, tools, the Web workstation itselfUpgrade dsh on the Unix-like machine
Bundles already in workstationdsh plugin --profile workstation add <package> or pnpm update

4. Use in the browser

  1. Open https://<hub-host>
  2. Sign in with a Hub user
  3. Use DSH Web as usual

Pick a workspace in the page. Sign out is at the bottom of the sidebar. Clicking an artifact previews code, images, and PDFs, and only files under the current working directory or a registered workspace (403 after realpath if still outside those roots). SVG is served as plain text (Content-Disposition: attachment) and is not opened as an active document on the Hub origin. Spreadsheets and large data files (csv, h5ad, rds, and similar) do not open.

The offline page is shown when no agent is online.

HTTPS

Public or non-loopback deployments must terminate TLS in front of Hub. The Hub process speaks HTTP only and binds 127.0.0.1 by default.

hub.yaml:

host: 127.0.0.1
port: 8787
agentSecret: "$2a$12$...."
trustedProxies:
  - 127.0.0.1
users:
  alice: "$2a$12$...."

After trustedProxies lists the reverse proxy's TCP address, Hub reads X-Forwarded-For (rightmost hop) and X-Forwarded-Proto (for Secure cookies). Those headers are ignored when the list is empty, so a client cannot spoof them to bypass login / /agent rate limits.

Caddyfile:

hub.example.com {
    reverse_proxy 127.0.0.1:8787
}
dsh-hub serve --config /path/to/hub.yaml
dsh-hub connect https://hub.example.com --user alice

For a short cleartext trial on a trusted network, set YAML host: 0.0.0.0 and allowPlainHttp: true, or serve --allow-plain-http; connect to a non-loopback http:// needs --allow-plain-http.

Start Hub on boot

/etc/systemd/system/dsh-hub.service:

[Unit]
Description=DSH Hub
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/deepseek-harness/hub
ExecStart=/usr/bin/dsh-hub serve --config /etc/dsh-hub/hub.yaml
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now dsh-hub

connect on the Unix-like machine also needs to stay running (tmux, a systemd user service, or a cluster job). connect and dsh must be on the same machine and uid.

Commands

HostCommand
Hubdsh-hub serve
Hubdsh-hub hash (bcrypt hash for hub.yaml)
Unix-like machinedsh-hub connect <url>
dsh-hub --help
npm test

Troubleshooting

SymptomWhat to do
Do not know where hub.yaml isHub host only. Default $PWD/hub.yaml. See the config: line serve prints
Only this machine can open the login pagePublic access should go through a TLS reverse proxy; do not set host to 0.0.0.0 unless allowPlainHttp is also set
dsh not foundPut dsh on PATH, or pnpm install && pnpm run build at the harness repository root
Cannot find package '@dsh-hub/preview' (or logout / webserver-unix)Global dsh resolves bare names from its own install, not the Hub repo. Upgrade this repo and run dsh-hub connect again; connect links the three packages into dsh node_modules. If dsh is in a read-only prefix, install it somewhere writable and connect again
--patch cannot find a packageWrite the plugin name as an absolute path, not a bare package name
Plugins gone after another connectdsh.profile.bundles and cordis.patch.yml are not emptied; if you mixed npm and dsh plugin, look at node_modules in the same directory. See section 3
dsh plugin cannot find pnpmPut pnpm on PATH, then install the bundle
Workstation start timed outdsh did not come up; see stderr on the same terminal
Loading hub.yaml complains about bcryptThe field must be a hash from dsh-hub hash or first serve, not plaintext
Signed in but always offlineconnect is not running, the username does not match hub.yaml, the Unix-like machine cannot reach Hub, or the agent secret is not the DSH_HUB_AGENT_SECRET from first serve
Lost DSH_HUB_AGENT_SECRETThe yaml cannot recover the plaintext. dsh-hub hash a new agentSecret and update the Unix-like machine
Password file errorMode must be 0600; others must not be able to read it
Artifact preview keeps loadingForce-refresh the browser and try again
Preview 403 path not allowedThe file must be under the current working directory or a registered workspace; a symlink must not point outside those roots
Socket directory error (symlink / not this user / not 0700)Remove the occupied /tmp/dsh-hub-<uid>, or set XDG_RUNTIME_DIR and connect again