Remote DeepSeek Harness with full server-side API access (`settings.*` / `credentials.*` / `host.listDirectory`). Token-gated reverse proxy, per-device sessions, optional approval, WebSocket/SSE, sidebar panel.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-full-remote
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:JUANWANG-BUAA/dsh-full-remote
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).
README
English | 中文
Remote access to DeepSeek Harness Web with full server-side API access.
When you reach Harness through a generic tunnel, methods such as
settings.*, credentials.*, and host.listDirectory return 403. That is
not a bug in the tunnel: Harness's browser trust fence only reads HTTP
headers, and a public Host/Origin fails it. This plugin rewrites Host and
Origin to 127.0.0.1:<backendPort> on the way through, so the fence lets
those privileged methods through — the same methods every other remote
plugin leaves 403.
The fence no longer protects the remote side, so this plugin puts a stronger door in front: a 192-bit access token, per-device credentials (hash at rest), failed-login rate limits, and optional first-visit approval.
The claim is server-side API completeness, plus two client behaviors this
plugin pins for a phone on the LAN: official settings persist, and "Add
workspace" uses the in-app directory browser instead of a native chooser on
the host display. The index tap declares __DSH_FULL_REMOTE_TRUSTED__ and
wraps window.__ModuleLoader__ so connection.isLoopback is true before
official settings plugins bind. Host path-open from a remote browser then
acts on the host machine. The durable upstream fix is still a
__DSH_BOOT__ trust field. See
Known Limitations.
The plugin does not launch or manage tunnel software. Point frp, ngrok, cloudflared, Tailscale, SSH, or anything else at the local target shown in Settings → Reverse proxy.
Screenshots
Control-page shots are from a clean harness profile (no extra plugins). The phone workspace shot is a real device capture of the in-app directory browser this plugin pins.
| Feature | Screenshot |
|---|---|
| Settings → Reverse proxy — status, tunnel target, copy | ![]() |
| Listen address — non-loopback warning before applying | ![]() |
| Access token — reveal and rotate | ![]() |
| Remote login gate — desktop | ![]() |
| Remote login gate — mobile (390×844) | ![]() |
| Add workspace on a phone — in-app directory browser | ![]() |
What you get
- Authenticated reverse proxy for HTTP, SSE, and WebSocket.
- Privileged Harness APIs that other remote setups 403:
settings.describe/update/replace/mutate,credentials.describe/set/unset,host.listDirectory/pickDirectory/openPath,agentPreset.*,llm.discoverModels. - Per-device sessions: the panel lists connected devices and can kick any one instantly.
- Optional first-visit approval: new devices wait until you approve them locally.
- Runtime listen address with persistence and automatic rollback.
- Guarded
crypto.randomUUID+AbortSignal.anypolyfills so remote file attachments keep working on plain HTTP. - Official settings persist on a tunnel hostname; "Add workspace" uses the in-app directory browser so a phone does not depend on a native chooser on the host display.
Security model
Harness trusts its loopback Web endpoint. Rewriting Host/Origin is what restores the privileged APIs, and it is also what disables the original fence for remote clients. The substitute gate:
- a 192-bit access token, stored locally with mode
0600; - remote browsers exchange the token for an HttpOnly, SameSite session cookie carrying a per-device secret; only its hash is stored;
- failed logins cost a fixed delay plus a per-IP
429lockout; - Harness control routes are never forwarded through the proxy;
- start, stop, token reveal, rotation, and listen changes require a direct loopback request with a CSRF-resistant control header and loopback Origin;
- spoofable forwarding and hop-by-hop headers are stripped;
- the proxy's own session cookie never reaches the backend; upstream
set-cookieis stripped; - request bodies are size-limited on the stream itself.
Origin rewrite is a configuration-plane change, not a session-plane one: every proxied request, including ones that mutate settings or credentials, presents a loopback Origin to Harness. That is the point of the plugin. Keep the token secret. Terminate TLS on the public side of your tunnel.
Install
dsh plugin --profile web add dsh-full-remote
dsh --profile web
From this repo, before the package is on npm:
pnpm pack
dsh plugin --profile web add ./dsh-full-remote-0.2.2.tgz
Git installs (dsh plugin add github:JUANWANG-BUAA/dsh-full-remote#<sha>)
run the self-contained prepare script; pnpm ≥10 users must allow the
build with allowBuilds: { dsh-full-remote: true } in the profile
workspace.
Open http://127.0.0.1:3080. Open Settings — 反向代理 is the last
item in the left nav (English: Reverse proxy), below the official
pages. Start the endpoint, copy its local target, and configure your
tunnel:
# Examples only — the plugin does not run these commands.
cloudflared tunnel --url http://127.0.0.1:3081
ngrok http http://127.0.0.1:3081
ssh -R 8080:127.0.0.1:3081 user@example-host
The remote browser receives a token login page before any Harness content.
Upgrade
An already-installed profile does not fetch new versions when
dsh web starts. After a release on npm, run this on the machine that
hosts Harness:
dsh plugin --profile web update dsh-full-remote
Then restart dsh web. add is for first-time installs; running it again
does not reliably bump a version pinned in the lockfile.
A local link: install ignores npm: pnpm run build in the repo, then
restart dsh web.
Choosing a listen address
Binding any IP already works — via listenHost in cordis.yml, or the
listen-address fields on the settings page. Runtime values win over config
and persist across restarts.
| What you type | What it means | When to use it |
|---|---|---|
127.0.0.1 (default) |
Loopback only. The tunnel process must run on the same machine. | Almost always, if cloudflared/ngrok/frp/SSH runs locally. |
A concrete LAN IP (192.168.x.x) |
Listen on that NIC only. The panel shows that address; copy-paste works. | Phone on the same Wi-Fi, no tunnel. Re-fill after DHCP/Wi-Fi changes. |
0.0.0.0 / :: |
Bind every interface. Not a connectable destination. The panel copies a reachable address (first non-internal IPv4) and still shows the real bind. | You want every NIC, including VPN, and accept that. Prefer a concrete LAN IP when you can. |
0.0.0.0 is "bind all interfaces", not "the address my phone should
open". Filling it and pasting the result into cloudflared is undefined
on some platforms. The panel will not offer http://0.0.0.0:… as the
copyable target.
Leave backendHost at 127.0.0.1. It is the TCP target for the
Harness process, not a listen address. A wildcard there is rejected at
load time; Host/Origin rewrite always uses 127.0.0.1 regardless.
Publish on a different IP / port (runtime)
Open Settings → Reverse proxy and edit Listen address: set the
IP/host and port (0 picks a free port), then press Apply listen
address (zh locale: 应用发布地址). The override is written to the
state file, applied immediately (restarting a running proxy), and used
again after Harness restarts. If the new address cannot bind, the plugin
rolls back to the previous working address and shows a toast on the page.
If Start proxy looks like a no-op, the panel now keeps a toast with the next step. Typical causes:
- The listen port is already in use: change it (for example
3081), apply, then start. - The listen address equals the Harness backend: that would loop; pick a different port.
- The control panel was opened from a tunnel hostname: start and stop
from the local
127.0.0.1window.
Separate mobile and desktop profiles
Harness composes Client plugins once per process, not once per browser viewport. A second Harness process is the supported way to give phones a leaner UI, but that process still needs a Web UI.
Copy or reuse a profile that already boots the Web app (typically your
working web profile), install this plugin there the same way as
Install, and start it on another port. Point the tunnel at
that process's proxy target. Desktop browsers keep using the full web
profile.
Do not add this plugin to a fresh empty profile: it injects webServer,
and a row waiting on a missing service fails the whole boot.
Configuration
- id: reverse-proxy
name: dsh-full-remote
config:
listenHost: 127.0.0.1
listenPort: 3081
backendHost: 127.0.0.1
backendPort: 0
autoRestore: true
maxRequestBytes: 16777216
upstreamTimeoutMs: 15000
sessionMaxAgeSeconds: 2592000
cookieName: dsh_reverse_proxy_session
maxHeaderSizeBytes: 16384
headersTimeoutMs: 15000
keepAliveTimeoutMs: 5000
loginDelayMs: 250
loginMaxAttempts: 5
loginLockoutSeconds: 300
approvalMode: false
maxSessions: 16
logRequests: false
stateFile: ""
listenHost/listenPortare defaults; the panel can override them at runtime and the override persists. See Choosing a listen address.backendPort: 0follows the activewebServer.port.listenPort: 0chooses a free port and displays it in the UI.stateFile: ""uses$DSH_HOME/reverse-proxy.json.backendHostmust be a loopback address. Wildcards (0.0.0.0,::) fail the plugin load. TCP still uses this host; Host/Origin rewrite always uses127.0.0.1.approvalMode: trueholds every new device on a waiting page until it is approved from the panel.- Web profile only. A headless profile has no UI to remote, and a row
waiting on
webServerfails the whole boot.
The plugin id (reverse-proxy), cookie name, control prefix, and state
file name are frozen across the npm rename from dsh-reverse-proxy.
Existing sessions and state files keep working.
Installing this plugin also pins the in-app directory picker so a phone can add a workspace:
- disables the
directory-pickerrow (directory-picker-autowould open a native chooser on the host display); - inserts
directory-picker-browseandui-directory-picker-browse.
Do not re-enable directory-picker in the same profile — it would steal
directoryPicker, and "Add workspace" would pop on the host again.
Compatibility
The control page mounts on the settings.section slot
(order 30, after the official General / Models / Plugins / Agent presets
pages).
- Our client peer range is
>=0.1.0-rc.5 <0.2and resolves on npm today (0.1.0-rc.6is published for the runtime/slots packages). - Rows that cannot activate fail the whole harness boot (strict activation gate).
Development
Dependencies install from npm; the repository is self-contained.
pnpm install # from the frozen lockfile
pnpm run check:ci # lint + typecheck (CI declarations) + tests + build
pnpm run check # same, but typecheck uses real harness types when a
# sibling deepseek-harness checkout exists
pnpm run bootstrap # optional: clone + build the harness checkout for real types
pnpm pack --dry-run # inspect the published tarball contents
CI runs check:ci plus a real-boot smoke job on every push and pull request
(.github/workflows/ci.yml). The smoke job installs the bundle through
dsh plugin add and exercises the control surface, login gate, rate
limiter, and index polyfill against a live harness composition
(scripts/smoke.mjs).
The package has a Host entry (lib/index.js) and an official DeepSeek
Harness Client entry (lib/client.js). The browser UI registers only
through the official settings.section slot (id reverse-proxy,
order 30). It does not guess sidebar DOM geometry.
Control API
All endpoints live under /dsh-reverse-proxy on the main DeepSeek Harness
Web server, are loopback-only, and are never forwarded through the
public proxy. Mutations and token reveal require the
x-dsh-reverse-proxy-control: 1 header and a loopback Origin.
| Method | Path | Body | Returns |
|---|---|---|---|
GET |
/dsh-reverse-proxy/status |
— | snapshot (enabled, running, target, backend, listen, reachables, wildcard; last start reason after a failed start) |
GET |
/dsh-reverse-proxy/token |
— | { accessToken } (control header required) |
POST |
/dsh-reverse-proxy/start |
— | snapshot |
POST |
/dsh-reverse-proxy/stop |
— | snapshot |
POST |
/dsh-reverse-proxy/rotate-token |
— | snapshot + new accessToken |
POST |
/dsh-reverse-proxy/listen |
{ "host": "127.0.0.1", "port": 3081 } |
snapshot (port 0 = pick a free port) |
GET |
/dsh-reverse-proxy/sessions |
— | { sessions: [{ id, label, status, createdAt, lastSeenAt }] } |
POST |
/dsh-reverse-proxy/sessions/approve |
{ "id": "…" } |
{ "ok": true } (pending → active) |
POST |
/dsh-reverse-proxy/sessions/revoke |
{ "id": "…" } |
{ "ok": true } (device loses access immediately) |
On the proxy itself, /_dsh_reverse_proxy/healthz answers {"ok":true}
without a token (load-balancer probe). The login page lives at
/_dsh_reverse_proxy/login.
Model Experience
This plugin adds no model-visible prompt, tool, or session content. Token and proxy status exist only in the human Web control surface, so token and KV-cache effects are zero.
Known Limitations and Deferred Work
- Remote settings persist by trusting the page at connection provide.
The proxy already rewrites Host/Origin so
settings.*returns 200. The index tap sets__DSH_FULL_REMOTE_TRUSTED__and wraps__ModuleLoader__so official settings / models / locale bind with host persistence.connection.isLoopbackstays true on that page — "open on host" from a phone acts on the host desktop. The durable upstream contract is still a__DSH_BOOT__trust field. - Add workspace uses the in-app directory browser. Installing this
plugin disables
directory-picker-auto(which would pop a native chooser on the host display whenever webServer binds127.0.0.1) and mounts the browse backend + UI pair. A phone can pick a folder; the local Mac GUI uses the same in-app dialog instead of Finder. - Settings left-nav icon is the harness default gear.
SettingsRootonly ships glyphs for official section ids. The two-node bridge glyph lives on the reverse-proxy page itself. - Start / stop / token / listen from a tunnel hostname is 403 by design
(control routes are never forwarded). Operate those from the local
127.0.0.1window; the settings page then explains that with a toast. GET /tokenis loopback HTTP with no caller identity. The endpoint now requires the same control header and loopback Origin as mutations, which stops a barecurl. Any local process that can send that header can still read the token. The state file is0600; treat the local machine as the trust boundary.- Origin rewrite is configuration-plane: Harness sees a loopback Origin on every proxied request, including settings and credentials mutations.
- The public URL is owned by the chosen tunnel and cannot be discovered by this provider-neutral plugin.
- The login cookie cannot always carry
Securebecause TLS usually terminates outside the local proxy. - The proxy strips upstream
set-cookieand its own session cookie. - Stopping the proxy destroys both ends of every upgraded WebSocket session. The backend's own upgraded socket may linger until its handler observes FIN.
- HTTP/2 terminates at the tunnel or browser edge; the local proxy forwards HTTP/1.1, SSE, and WebSocket.
- Web profile only. Do not install into headless.
Contributing
Contributions are welcome — see CONTRIBUTING.md for the development setup, checks, and conventions.
Security
Security issues are handled privately — see SECURITY.md for the disclosure process and the supported-versions policy.
License
MIT © 2026 JUANWANG-BUAA
Links
More in this category
liustack/modlens★ 1837
Vision bridge for text-only models: paste an image, get structured JSON evidence (OCR, layout, semantics).
Anionex/dsh-vision-toolkit★ 422
Vision tasks for text-only models: intent-aware image Q&A, long-screenshot OCR, UI reproduction, grounding, and pixel diff.
superdesigndev/treg★ 416
Tool catalog for agents: search ~2,600 external endpoints (SEO and SERP, backlinks, social, people and company enrichment, ad libraries, scraping) by the task you want done, read each one's parameters and per-call price, then call it with the credential injected server-side. Ships the skill plus an MCP row that stays disabled until TREG_TOKEN is set.
Lum1104/dsh-browser★ 156
Chrome sidebar extension that lets DSH operate your browser directly, no vision capabilities required.
zhaoolee/notes★ 142
Export DSH conversations as Smartisan Notes-style PNGs, or create and update Markdown notes in a configured account-scoped workspace.
ysr666/dsh-vision-router★ 135
Free vision for text-only agents: built-in keyless vision chain plus pixel tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots); paste an image to use it.





