Back to home

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 dsh web builds), 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 / keepAliveCountMax make 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, lowering keepAliveIntervalSec to 10 and backoffBaseMs to 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):

VariableMeaning
DSH_MOBILE_PUBLIC_HOSTPublic origin, bare host or host:port (required)
DSH_MOBILE_RELAY_HOSTRelay server address (required)
DSH_MOBILE_RELAY_USERSSH login user on the relay server (required)
DSH_MOBILE_TOKENGate 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 HttpOnly and SameSite=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 clientsshBin must resolve on the machine running dsh; a missing binary fails loud at spawn and retries with backoff, and the README's manual ssh -N -R command 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 web process; 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 mobileAccess service only; a settings card is deferred work.