api-relay-audit
toby-bridges
Local security audit for AI API relays and LLM proxies: detects prompt injection, model substitution, tool-call rewriting, SSE anomalies, error leakage, and Web3 wallet risks.
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:JUANWANG-BUAA/dsh-full-remote
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
Listed in awesome-dsh-plugin · DeepSeek Harness plugin
Current release: v0.3.12 · Distributed through GitHub Releases
English | 中文
dsh-full-remote is a plugin for
DeepSeek Harness. It
places an authenticated reverse proxy in front of the Harness Web server,
so the Web UI can be used through a public tunnel or from a device on the
local network while privileged APIs such as settings, credentials, and
directory browsing remain available.
curl -fLO https://github.com/JUANWANG-BUAA/dsh-full-remote/releases/download/v0.3.12/dsh-full-remote-0.3.12.tgz
dsh plugin --profile web add ./dsh-full-remote-0.3.12.tgz
dsh --profile web
In Settings → Reverse proxy, press Start proxy, then Start Cloudflare quick tunnel and scan the generated QR code. The invite is one-time and never contains the standing access token. For a controlled network, point an existing SSH, frp, ngrok, Tailscale, or cloudflared tunnel at the proxy target shown in the panel instead.
The quick tunnel is optional and temporary, not a managed production deployment. Read Security model before exposing a listener to the Internet. For composition details, see Compatibility and composition.
| Desktop control panel | Mobile workspace |
|---|---|
![]() |
![]() |
| Phone confirmation sheet | Remote desktop confirmation |
|---|---|
![]() |
![]() |
DeepSeek Harness binds its Web server to a loopback address and only
accepts privileged requests when the Host and Origin headers refer to
a loopback address. When the UI is reached through a generic tunnel, these
headers carry the public hostname and the trust check fails. The page
loads, but the following methods return 403:
settings.*credentials.*host.listDirectory| Approach | Result |
|---|---|
Generic tunnel (SSH port forward, Caddy, binding 0.0.0.0) |
Page loads; settings.* / credentials.* / host.listDirectory return 403 |
| LAN-only plugin without authentication | Usable on the local network; not suitable for public exposure |
| Password prompt without header rewriting | Requests are authenticated, but the privileged APIs remain blocked |
The plugin inserts a reverse proxy between the tunnel and the Harness Web server. The proxy:
Host and Origin to 127.0.0.1 before forwarding, so the
privileged APIs pass Harness's trust check;Because the rewrite disables Harness's original trust check for remote clients, the plugin provides its own access-control layer in its place. This layer is described under Security model.
The plugin can optionally start a temporary Cloudflare quick tunnel. Any managed tunnel (cloudflared, ngrok, frp, SSH, Tailscale) can also point at the local endpoint it publishes.
flowchart LR
A[Phone or remote browser] --> B[Public tunnel<br>cloudflared / ngrok / frp / SSH]
B --> C[dsh-full-remote<br>127.0.0.1:3081<br>authentication + header rewrite]
C --> D[DeepSeek Harness Web<br>127.0.0.1:3080]
127.0.0.1:3081 by default).Host/Origin to loopback, removes untrusted
headers, and forwards the request to the Harness Web server at
127.0.0.1:3080. Compressible HTTP responses (HTML/JS/CSS/JSON/SVG,
≥1 KB) may be gzipped; SSE and WebSocket are not. Hashed /assets/*
files may receive a long-cache header. See
HTTP gzip.settings.describe / update / replace / mutatecredentials.describe / set / unsethost.listDirectory / pickDirectory / openPathagentPreset.*, llm.discoverModels0600; reveal and
rotation are performed from the local paneltrustForwardedFor to use real client IPs from a trusted local
tunnel in CIDR / rate-limit / audit via its rightmost X-Forwarded-For
value; CF-Connecting-IP is a separate Cloudflare-only opt-in, and
loopback or malformed forwarded values are never trustedsettings.describe with the same Host/Origin
rewrite the proxy usestlsCertFile / tlsKeyFile)/_dsh_reverse_proxy/healthzset-cookie is removedvendor-*.js −75.7%. Tiny JSON grows,
so it is not compressed. Issue #11's "95%+" is not a general result.
Off: compressResponses: false. Details: HTTP gzipCache-Control on hashed /assets/* (not index.html or
/api). Off: cacheHashedAssets: falsehttps://…trycloudflare.com address — no public IP or port
forwarding requiredcloudflaredPath → PATH → a pinned
(2026.8.2), SHA256-verified download cache; failed checksums are
discarded/_dsh_reverse_proxy/home:
device facts (label, login IP/time, expiry estimate, security
posture), self-rename, and self-logout (revokes only this device)/; the original flow is unchangedask_user_question option lists, and plan reviews
appear as a confirmation sheet on the remote page: a bottom drawer
on a phone, a centered card on a wider remote window. You can choose
and submit there; you do not have to go back to the host. Custom
answers wrap; Shift+Enter inserts a newline. The official composer
still only sits on the current sessiondeepseek-v4-flash-vision-exp (and other
image-capable routes) goes through the same authenticated /api
path. The default body cap is 160 MiB, matching Harness. Raster
image responses are not gzipped^22.19.0 || >=24webServer and
Host connection services and is not intended for headless profiles.
Verified against 0.1.2-rc.1 (npm next dist-tag), with a compatibility
path for 0.1.1-rc.1/rc.2.curl -fLO https://github.com/JUANWANG-BUAA/dsh-full-remote/releases/download/v0.3.12/dsh-full-remote-0.3.12.tgz
curl -fLO https://github.com/JUANWANG-BUAA/dsh-full-remote/releases/download/v0.3.12/SHA256SUMS
shasum -a 256 -c SHA256SUMS
dsh plugin --profile web add ./dsh-full-remote-0.3.12.tgz
dsh --profile web
The current release is v0.3.12. npm publication is separate and currently lags behind the GitHub release, so use the attached tarball above when installing or upgrading this version.
http://127.0.0.1:3080.# Examples only. The plugin does not execute these commands.
cloudflared tunnel --url http://127.0.0.1:3081
ngrok http 3081
For devices on the same network, set the listen address to a LAN IP instead of using a tunnel.
The package was previously published as dsh-reverse-proxy; that legacy name
is deprecated. Install dsh-full-remote for new deployments.
On the settings page, press Start proxy to start the listener and Stop proxy to stop it.
| Bind | Purpose |
|---|---|
127.0.0.1 (default) |
The tunnel runs on the same machine |
192.168.x.x |
A device on the same network, without a tunnel |
0.0.0.0 / :: |
Bind every interface. This is not an address to open; the panel reports a separate reachable address. |
The listen address can be changed at runtime and persists across restarts. If a new address fails to bind, the proxy rolls back to the previous working address.
The copyable tunnel target (and any extra reachable URL the panel
lists) is what a remote client should open. Binding 0.0.0.0 only
listens; it is not a URL.
backendHost is the address the proxy connects to, not the address it
listens on. Keep it at 127.0.0.1.
The QR encodes a one-time login URL. Public / reachable Origin is the
host the scanning device will request: the tunnel's https://…, or the
LAN URL from the panel. Leave it empty only when the tunnel target above
is already that address.
Do not put 127.0.0.1 in Origin. That address is the Harness machine; a
phone would open its own loopback and never reach the proxy.
Then press Generate invite. After a scan (or opening the link) the login page submits once. The invite expires in 15 minutes, works once (same-IP retries within 60 s reuse the original session), and does not contain the standing token. Invites can only be generated while the proxy is running.
Download the current GitHub release and add its tarball to the existing web profile:
curl -fLO https://github.com/JUANWANG-BUAA/dsh-full-remote/releases/download/v0.3.12/dsh-full-remote-0.3.12.tgz
dsh plugin --profile web add ./dsh-full-remote-0.3.12.tgz
Then restart dsh web. This replaces an older registry-installed or tarball
version with v0.3.12.
The plugin panel has a Language / 语言 selector: Auto, English, or 中文. Auto follows the Harness locale when available, otherwise your browser language (English for non-Chinese browsers). An explicit choice is saved per browser and origin and also applies to remote confirmation overlays. Login, approval-wait, and device-home pages continue to follow the browser’s HTTP language header.
The gallery is hosted in the repository; the release package keeps only runtime files and links back here so installation stays small.
The full settings page: running status and fence self-check, listen address, recommended setup, tunnel target, one-click quick tunnel, one-time invite QR, access token, connected devices with source IPs (inline rename), and the audit viewer.

| One-time phone invite (QR) | Connected devices with inline rename |
|---|---|
![]() |
![]() |
| Login page | Control panel | Add workspace |
|---|---|---|
![]() |
![]() |
![]() |
When the model asks a question, requests a tool approval, or presents a plan review, the remote browser shows its own sheet. You do not have to look at the host display.
| Phone bottom sheet | Remote desktop card |
|---|---|
![]() |
![]() |
The token login (with an opt-in Device home button), the device home itself, and the first-visit approval wait page.
| Device home | Waiting for approval |
|---|---|
![]() |
![]() |
Common options:
- id: reverse-proxy
name: dsh-full-remote
config:
listenHost: 127.0.0.1
listenPort: 3081
approvalMode: false # true: approve each new device locally
allowedCidrs: [] # e.g. ["192.168.1.0/24"]; empty: any IP after login
trustForwardedFor: false # true: trust rightmost X-Forwarded-For from a trusted local tunnel
trustCloudflareConnectingIp: false # true only with trustForwardedFor for a local Cloudflare connector
upgradeMaxAttempts: 10 # failed WebSocket upgrades before lockout
upgradeLockoutSeconds: 300 # lockout for repeated failed WebSocket upgrades
headersTimeoutMs: 15000 # timeout for request headers
requestTimeoutMs: 300000 # timeout for the complete request (headers + body); covers remote vision uploads
upstreamTimeoutMs: 15000 # TCP connect + first POST byte after the body; not applied to SSE GET
commandTimeoutMs: 300000 # first POST byte for host commands (/compact): /api/commands/execute, or a session.prompt whose single text part starts with "/"
maxRequestBytes: 167772160 # 160 MiB; matches the Harness /api image envelope
sessionIdleSeconds: 0 # 0: off; otherwise idle timeout in seconds
auditLog: true
allowTokenRead: false # safer default; enable only for local token re-read
cloudflaredPath: "" # optional path to cloudflared for the one-click tunnel
tlsCertFile: "" # optional local HTTPS
tlsKeyFile: ""
compressResponses: true # gzip JS/CSS/JSON/HTML ≥1KB; skip SSE/WebSocket/fonts/gate pages
cacheHashedAssets: true # immutable Cache-Control on hashed /assets/* only
The complete option list, with defaults and validation, is defined in the
package Config schema (src/config.ts) and
src/config-validation.ts (source is not included in the published package).
Two points to note:
DSH_FULL_REMOTE_USE_NATIVE_PICKER=1 before boot only
when you deliberately want the host's native chooser and do not need
remote directory browsing.backendHost must remain a loopback address. A wildcard or non-loopback
value is rejected at load time.The Host/Origin rewrite restores the privileged APIs and, at the same time, disables Harness's original protection for remote clients. The access-control layer provided by this plugin consists of:
0600;HttpOnly, SameSite=Strict session cookie per device, carrying a
per-device secret of which only a hash is stored;429 lockout on failed logins;/dsh-reverse-proxy/*), which require a
control header and are never forwarded through the public proxy;trustForwardedFor: when enabled, only a loopback peer's
rightmost X-Forwarded-For value is trusted for CIDR / rate-limit / audit.
CF-Connecting-IP needs the separate, Cloudflare-only
trustCloudflareConnectingIp opt-in; loopback or malformed values are
never trusted. Keep both disabled for direct LAN access.The access token must be treated as a secret. Terminate TLS on the public
side of the tunnel. For LAN use without a tunnel, set
tlsCertFile / tlsKeyFile (for example with
mkcert).
Public exposure checklist. Whoever holds the token controls the whole Harness — credentials and settings included — so for anything reachable from the internet:
approvalMode: true, so a new device stays pending until you
approve it in the local panel (the panel shows a warning whenever the
quick tunnel is online with approval off);allowedCidrs to pin the entry to known networks;auditLog: true (the default) and rotate the token after invites
outlive their need.0.1.0-rc.8 and later, that pin must survive the official ModuleLoader
create() replacing load; otherwise Settings → Models shows
settings are unavailable in this browser. "Open on host" from a phone
acts on the machine running Harness.allowTokenRead defaults to false. When explicitly enabled, GET /token
is served over loopback HTTP, so any local process that sends the control
header can read the token; rotation always returns the replacement token.127.0.0.1 to the proxy. allowedCidrs and per-IP
login lockout therefore apply to the tunnel as a whole unless
trustForwardedFor: true is set behind a trusted local edge.cloudflaredPath). For a stable daily entry, bring your own
frp / ngrok / named tunnel.pnpm pack
dsh plugin --profile web add ./dsh-full-remote-0.3.12.tgz
Git installs run the prepare build. On pnpm ≥ 10 allow it:
allowBuilds:
dsh-full-remote: true
pnpm install
pnpm run check:ci
check:ci runs lint, typecheck, unit and client tests, and a build. CI
adds a real dsh plugin add smoke test against a live Harness
composition. .github/workflows/canary.yml runs a weekly smoke test
against the harness default-branch tip.
The loopback control API lives at /dsh-reverse-proxy/* and is never
forwarded through the public proxy. The settings page is the intended
interface; the raw routes are rarely needed. For example, recent audit
events can be read with GET /dsh-reverse-proxy/audit?limit=50&event=login.ok
from the local control surface.
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: mobile-ui、security。