0xshawn/dsh-remote-access ↗★ 0

dsh-remote-access

非本地主机访问Web UI配置工具 适合需要通过反向代理、SSH隧道或局域网远程访问DSH Web UI的用户。

包名
dsh-remote-access
兼容性
待验证
版本
0.1.0
许可证
MIT
最近更新
2026年10月26日

同名包的其他仓库

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:0xshawn/dsh-remote-access

dsh-remote-access

license node

Serve the DeepSeek Harness (dsh) Web UI through a non-localhost authority — a reverse proxy, SSH tunnel, or LAN address — without patching the installed dsh.

中文文档

What this fixes

Stock dsh web is built for a browser on the same machine. Three things break when the browser reaches it through any other authority:

ProblemStock dshThis plugin
/api Host/Origin fenceaccepts loopback, the listener's bind literal, and --trusted-host only; every RPC answers 403 for a proxy authorityappends DSH_REMOTE_ACCESS_TRUSTED_HOSTS authorities to the same list (--trusted-host keeps working)
Page view (connection.isLoopback)derived from location.hostname; a proxied page is treated as non-loopback, so on dsh 0.2.0 settings stay in process-local memory (settings are unavailable in this browser on the Models page) and on every version the host-document action is hiddeninjects __DSH_TRANSPORT__ = { ownsHost: true }, the host-owned transport hook dsh already uses for its desktop shell, so authenticated pages report the loopback view
Launch tokenrandom per process; the only way to find the login URL after a restart is the service logpins DSH_WEB_TOKEN (or plugin config) across restarts

Authentication is unchanged: the browser still has to exchange the launch token for the signed, authority-bound session cookie, and the Host/Origin fence still runs before every /api request. This plugin removes deployment friction; it does not bypass access control.

Install

# from GitHub (public repository, no build step)
dsh plugin --profile web add git+https://github.com/0xshawn/dsh-remote-access.git

# from a local checkout
dsh plugin --profile web add /path/to/dsh-remote-access

Restart dsh web afterwards — dsh loads bundles at boot. Verify the row is active:

dsh --profile web --dump-config | grep -A6 'dsh-remote-access'

Remove with dsh plugin --profile web remove dsh-remote-access; the stock behaviour returns on the next restart.

Migrating from a source patch

If a deployment currently patches the installed dsh bundles (fixed launch token plus a forced loopback classification), the switch is:

  1. Stop applying the patch in the installer; npm install -g @deepseek-ai/dsh@ no longer needs a follow-up patch step.
  2. Install this bundle into the web profile (above) and restart dsh web.
  3. Keep the deployment's DSH_WEB_TOKEN; it is read by the same name. Keep --trusted-host or move the authorities to DSH_REMOTE_ACCESS_TRUSTED_HOSTS.
  4. Confirm the startup line still prints the pinned token: dsh web: ...?token=.

Configure

Environment

VariableValueEffect
DSH_REMOTE_ACCESS_TRUSTED_HOSTScomma-separated bare authorities, e.g. dsh.example.com,dsh.example.com:8443appended to --trusted-host; a port-less entry matches any port
DSH_WEB_TOKENnon-empty launch tokenpins the login token; empty/unset keeps the random per-process token

Both work in systemd Environment=/EnvironmentFile=, launchd EnvironmentVariables, Docker env, or a shell profile. Keep --trusted-host if you prefer flags; the expression appends the environment authorities to the invocation list. Note that a patch layer replaces the whole connection.config: if another layer (or a later profile patch) overrides connection, restate trustedHosts there — otherwise the environment authorities are dropped — and restate any other key you still need, such as cookieMaxAgeDays or recovery.

DSH_WEB_TOKEN="$(cat /etc/dsh/web-token)" \
DSH_REMOTE_ACCESS_TRUSTED_HOSTS="dsh.example.com" \
dsh web --host 127.0.0.1 --port 3080 --no-open

Malformed authorities (a scheme, path, userinfo, or zero-padded port) abort boot with the upstream validation error instead of being silently ignored.

Plugin config

Override the dsh-remote-access row in the profile patch ($DSH_HOME/profiles/web/cordis.patch.yml) for per-profile values:

- id: dsh-remote-access
  config:
    # false keeps the stock per-authority page view.
    loopback: true
    # Fixed token; empty falls back to DSH_WEB_TOKEN, then the random token.
    # Prefer DSH_WEB_TOKEN: profile configuration is plain text.
    launchToken: ''

Behind a reverse proxy

--public-url https://dsh.example.com/ advertises the address browsers use (printed URL, DSH_WEB_URL, runtime context); this plugin does not change that. The proxy must still:

  • preserve the browser-facing Host header (the fence compares it verbatim);
  • strip any mount prefix before forwarding to the listener;
  • forward WebSocket upgrades with Upgrade/Connection intact;
  • rewrite cookie Path to the mount, and add Secure on the HTTPS leg.

See upstream Publish the Web UI behind a reverse proxy for the full checklist. TLS termination, network isolation, and who may reach the port remain the operator's job.

How it works

The package is a dsh bundle: cordis.patch.yml is a patch layer over @deepseek-ai/dsh-base + @deepseek-ai/dsh-web-app, and two seams are used.

  1. Trusted hosts — patch layer. The connection row's trustedHosts gets the environment list concatenated onto the invocation list. The expression reads ctx.get('webRuntime') (dsh 0.2.0, which already carries the bind-derived LAN literals and CLI extras) or ctx.get('webStartup') (dsh 0.2.1); 0.2.0 provides both services, so runtime must win to keep a wildcard bind's LAN authorities. One patch covers both layouts. The Host/Origin fence and the allowsRemoteAuthorities classification are then upstream's own.

  2. Loopback view — index injection. The host plugin listens to webserver/index-inject and adds one inline classic script to the served index:

    if (globalThis.__DSH_TRANSPORT__ === void 0) globalThis.__DSH_TRANSPORT__ = { ownsHost: true };
    

    ClientTransportHooks.ownsHost is upstream's flag for "this page owns the Host", documented as reserved for a shell that assembles its own transport; upstream also states that served pages never carry the global. This bundle sets it deliberately to obtain owner parity: dsh-client-connection reports the loopback view without changing the page's HTTP + WebSocket carrier. The guard leaves any real shell-provided transport untouched, and the script runs before the shell entry and every client plugin.

  3. Launch token — service field. The plugin waits for ctx.connection and sets connection.browserAuth.launchToken before the loader settles, so both the printed authenticated URL and token validation use the pinned value. If a future dsh version hides that field, the plugin fails loudly instead of silently printing a random URL.

Security

  • The launch token is the credential. Anyone holding the printed ?token= URL can obtain a session cookie and full owner access. Share it deliberately. A pinned token is a long-lived credential — never commit it. Changing DSH_WEB_TOKEN and restarting invalidates the old URL but does not revoke cookies already issued (they live for cookieMaxAgeDays, 30 days by default); clear the site's cookies, or remove the browser-session credential record and restart, to revoke them.
  • Trusted hosts are not authentication. They only admit the authority to the Host/Origin fence; the signed session cookie still gates every RPC. Never expose the listener port to an untrusted network without TLS and a proxy you control.
  • Loopback parity is a privilege decision. On dsh 0.2.0 it makes settings host-backed for authenticated remote browsers; on every version it exposes local-only UI affordances such as Open configuration file, which opens the settings document in the host's editor. Remote browsers already hold owner credentials, but operators who want the stock least-privilege view can set loopback: false.
  • The loopback view goes beyond a documented client contract. Upstream documents ownsHost as reserved for a shell that assembles its own transport and says served pages never carry the global. This bundle sets it anyway; every current dsh consumer reads it only for isLoopback, but a future or third-party client plugin could treat it as proof of host ownership. loopback: false removes that exposure.
  • Plain HTTP leaks the token. Terminate TLS at the proxy or with --tls-cert/--tls-key; the upstream guide covers cookie Secure behaviour.
  • Do not add a CSP that blocks inline scripts. The loopback row is an inline classic script; dsh's own boot globals already require inline script permission, but a proxy-injected CSP without 'unsafe-inline' (or a nonce covering it) silently blocks the row while the page still loads.

Compatibility

  • Tested against dsh 0.2.1-rc.2, 0.2.1-rc.3, and 0.2.0-rc.2 (Web profile).
  • Declared range: engines.dsh >=0.2.0-0 replaces the dsh install but not the profile: the plugin keeps working after a dsh upgrade. Restart dsh web to load a new dsh version.
  • The token seam reads connection.browserAuth.launchToken. If dsh changes that shape, the plugin throws at boot when a token is configured; unset the token or update the plugin.
  • The pin runs once at startup. A host-plugin reload (HMR) can rebuild the Connection service from dsh's cached per-process token and will not reprint the startup URL; restart dsh web after changing the token or the dsh version.

Development

npm install
npm test        # node:test unit tests for config, token resolution, index injection

End-to-end check on an isolated home (no effect on your real profile):

export DSH_HOME=$(mktemp -d)
dsh plugin --profile web add "$PWD"
DSH_WEB_TOKEN=dev-token DSH_REMOTE_ACCESS_TRUSTED_HOSTS=dsh.test \
  dsh --profile web --host 127.0.0.1 --port 3199 --no-open
# then open http://:3199/?token=dev-token through your proxy

License

MIT © 2026 shawn