kk99668
dsh-mobile-access
dsh bundle: serve the DeepSeek Harness Web GUI to a phone through your own domain and server (token gate + supervised SSH reverse tunnel)
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 15, 2026
- Updated
- Aug 15, 2026
Introduction
dsh-mobile-access
English | 中文
The dsh mobile-access bundle: serve the Web GUI to a phone through your own domain and server. cordis.patch.yml rides over the web surface: it extends the connection row's /api browser-trust fence with the public authority the tunnel serves, and inserts this package's mobile-access glue plugin. That plugin starts a local token gate (src/gate.ts) in front of the webserver and a supervised SSH reverse tunnel (src/tunnel.ts) to the relay server, provides the mobileAccess service (the public host the fence accepts), registers the mobile-surface prompt section and the bash-visible DSH_WEB_PUBLIC_URL runtime variable when surfaceContext is true, and prints the public URL (plus a terminal QR code when qrCode is true) once the tunnel is established. On the relay server, Caddy terminates HTTPS for the domain and reverse-proxies into the tunnel; scripts/setup-server.sh installs that side. The step-by-step deployment runbook is DEPLOYMENT.md.
How it works
phone ── https://dsh.example.com ──> Caddy (HTTPS, on your server)
└──> 127.0.0.1:<relay-port> (sshd reverse tunnel)
└──> local token gate (dsh process)
└──> http://127.0.0.1:<web-port>
The tunnel is an ssh -N -R child owned by the plugin: it reconnects with exponential backoff, resets the backoff after a stable uptime, and dies with its fiber. The gate is the authentication layer the fence deliberately is not: every request and WebSocket upgrade must present the token (a cookie set by the login form, or Basic/Bearer credentials) before it is forwarded to the webserver. The public authority reaches the fence through the mobileAccess service, which the patch injects into the connection row so the fence's config expression can never resolve before the authority exists.
Weak-network behavior
The phone link is the constrained hop (a subway commute, for example); the tunnel and the relay sit on the two stable ends. Three mechanisms keep the GUI usable when that link degrades:
- The event streams compress when the host supports it. The WebSocket downlinks negotiate
permessage-deflate(shipped in dshwebbuilds), so the session event stream (repetitive JSON) shrinks several-fold on the phone's link. The browser negotiates the extension automatically. - The browser reconnects. The web client's connection controller re-establishes both event streams with exponential backoff (500 ms doubling to a 10 s cap) whenever the signal drops, so a tunnel outage or a dead zone recovers without a page reload.
- The tunnel heals itself.
keepAliveIntervalSec/keepAliveCountMaxmake ssh notice a dead relay fast (default 30 s × 3 probes), and the supervisor restarts the child with jittered backoff (backoffBaseMs..backoffMaxMs). On a flaky home uplink, loweringkeepAliveIntervalSecto 10 andbackoffBaseMsto 1000 shortens the blackout window at the cost of a few extra probe packets.
Install
The bundle is opt-in, on top of the shipped web profile. Install from this repository (a prepare script builds the plugin from source; pnpm ≥10 asks you to allow the build the first time — copy the package key pnpm prints into the profile's pnpm-workspace.yaml under allowBuilds and re-run), or from npm when published:
dsh plugin --profile web add github:kk99668/dsh-mobile-access
# or, once published to npm:
dsh plugin --profile web add dsh-mobile-access
The inserted row reads its config from environment variables (the app boot loads .env from the invoking directory and the Harness home):
| Variable | Meaning |
|---|---|
DSH_MOBILE_PUBLIC_HOST | Public origin, bare host or host:port (required) |
DSH_MOBILE_RELAY_HOST | Relay server address (required) |
DSH_MOBILE_RELAY_USER | SSH login user on the relay server (required) |
DSH_MOBILE_TOKEN | Gate secret, at least 8 characters (required) |
Unset required variables fail the boot loud with the field names. Every other setting is a validated plugin config field with a default: relayPort 22, remoteBindHost 127.0.0.1, remoteBindPort 3080, localPort (the active webserver port), gatePort 0 (OS-assigned), sshBin ssh, identityFile unset, keepAliveIntervalSec 30, keepAliveCountMax 3, connectTimeoutSec 10, backoff 2s..30s, stableUptimeMs 60s, runningAfterMs 3s, printUrl true, qrCode true, surfaceContext true. To change one, override the row's config in $DSH_HOME/profiles/web/cordis.patch.yml (a patch replaces the whole config, so restate every key):
- id: mobile-access
config:
publicHost: dsh.example.com
relayHost: vps.example.com
relayUser: dsh-relay
token: a-long-random-secret
remoteBindPort: 8443
identityFile: C:\Users\you\.ssh\id_ed25519
Server setup
Run once on the relay server (root): sudo ./scripts/setup-server.sh dsh.example.com 3080 ~/dsh-relay.pub. The script installs Caddy with automatic HTTPS for the domain, creates the dsh-relay user, installs the given public key with authorized_keys restrictions that allow only the reverse listen (permitlisten="127.0.0.1:3080", no shell, no agent, no X11), warns when GatewayPorts is enabled, and opens 22/80/443 in ufw. The key file argument is optional: any SSH login that can reach the server works, though the restricted relay user is the recommended shape. After the script, set the environment variables above, start dsh --profile web, and scan the QR code with the phone.
Security
- The token gate is the only thing between the public internet and the agent. Anyone who knows the token can control this agent — treat it as a credential.
- The gate's login cookie is
HttpOnlyandSameSite=Lax; the token never enters the URL, logs, or referrers. - The relay server binds the tunnel port to loopback only; the phone never talks to the tunnel directly, only to Caddy over TLS.
- The fence's privileged methods (
settings.*,credentials.*,host.*, ...) stay pinned to loopback even on a trusted-host deployment, so the configuration plane is not reachable through the tunnel. - Reconsider before exposing the harness from a shared or untrusted network. The web app itself has no user accounts; the gate is the boundary.
Model Experience
Mobile-surface context
What the model sees
When surfaceContext is true, the app:mobile-surface global section (order −97, right after the web-surface section) orients the model to the public origin, and DSH_WEB_PUBLIC_URL appears in the managed bash environment with its description, resolved per invocation:
Verbatim text for this field
The DeepSeek Harness Web GUI is also served at https://dsh.example.com through a secure tunnel. Users arriving there interact with the same harness and sessions as this local GUI.
Token effect
One prompt paragraph per session plus one managed-environment variable line, constant per process.
KV Cache effect
The section sits near the system prompt's head and is stable for the life of the process (the public host is a boot fact), so it does not invalidate the cache across turns.
Known Limitations and Deferred Work
- The tunnel requires an SSH client —
sshBinmust resolve on the machine running dsh; a missing binary fails loud at spawn and retries with backoff, and the README's manualssh -N -Rcommand is the diagnosis path. - The phone needs a browser login per device — the session cookie has no
Max-Age, so each phone authenticates once per browser profile; there is no persistent-device registry or per-device revocation. - Windows autostart is the user's own — the tunnel lives inside the
dsh --profile webprocess; keeping the GUI reachable means keeping that process running (a service wrapper is not shipped). - No in-GUI status surface — tunnel status is visible in the console log and through the
mobileAccessservice only; a settings card is deferred work.