DeepSeek Harness Plugin

GDWhisper/dsh-web-startup-auth

Stars ★ 50 Category Security & Permissions Added 2026-08-19

Replaces the dsh web startup to allow binding 0.0.0.0, gated by username/password login: signed session cookies, /api route protection, an auth tab in the settings panel, and a reset CLI that rotates the signing key to invalidate all sessions.

Install

# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)

dsh plugin --profile web add github:GDWhisper/dsh-web-startup-auth

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 — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).

README

中文 | English

A DSH (DeepSeek Harness) plugin that enables remote web startup with username/password authentication.

⚠️ Version tracking notice: This project only tracks the official next dist-tag (the pre-stable release channel) and does not follow the alpha preview channel (current baseline: dsh 0.2.0-rc.1, with all five @deepseek-ai/dsh-* dependencies bumped to ^0.2.0-rc.1; see docs/upgrade-dsh-0.2.0-playbook.md for the adaptation review and sentinels).

Login page

The stock @deepseek-ai/dsh-web-app/startup hard-rejects --host 0.0.0.0 for safety. This plugin replaces it and adds an auth layer (login/register page + signed session cookies), letting you safely expose dsh web on a LAN or any non-loopback interface.

Features

  • Remote startup: --host 0.0.0.0 works, replacing the stock launcher's hard rejection; --host :: (or any IPv6 literal, e.g. ::1, fd00::1) works too (#35; non-canonical spellings are canonicalized, zone-suffixed (%eth0) and IPv4-mapped (::ffff:a.b.c.d) ones are refused). On a pure-IPv6 network 0.0.0.0 binds IPv4 only and the GUI is unreachable; :: is dual-stack on Linux (net.ipv6.bindv6only=0) and covers both IPv6 and IPv4-mapped peers; on bindv6only=1 systems :: is IPv6-only.
  • Login/register page: A remote first visit guides you through setting the admin credentials, then shows the login page; matches DSH's black/white/blue style.
  • Password-free local access: the decision is made per request, not per bind address — a request is trusted only when its TCP peer address and its Host header are both loopback. A browser on the same machine opening http://127.0.0.1:<port>/ needs no registration or login; LAN clients and requests forwarded by a reverse proxy (Host names the public domain) always need a session.
  • Optional human verification (slider puzzle): a bot check that does nothing much, added at a reader's request — feel free to turn it on and see XD. Flip it on under Settings → Auth → Human verification and a login will ask you to drag a piece into the slot. Each image shows two holes: the piece and the real hole share the same shape (the tab side is drawn fresh per puzzle — right, left, top, bottom, or no tab at all, 5 in all), while the decoy's shape is always different — the piece only fits the hole with the same shape (pure decoration; scripts sail past it anyway). "Does nothing much" is measured, not modesty: the answer has to be drawn in the picture (a human needs to see where to drag), so a few dozen lines of code read it straight out — three conventional variants of this implementation fall to a ~30-line script at 100% (darkened slot), 100% (outline only) and 97% (noise-hardened — and that one a human cannot solve either). The 4-digit image captcha tried earlier fell to template matching at 96.7%. A toy, not a security boundary. See docs/agent/human-verification.md.
  • Hardened login rate limiting: 5 failures per IP per 10 minutes locks that client out for 30 s, plus a global exponential backoff — once failures across all sources pass 20, the penalty doubles per step (5 s → 10 s → … capped at 5 minutes), which is what stops distributed stuffing from a thousand addresses; 429 carries Retry-After. A genuine loopback caller is exempt from the global penalty (unless "require login on loopback" is on), so an attacker cannot use it to lock the administrator out of their own machine.
  • Session authentication: Signed session cookie (dsh_sid, default 14-day expiry — configurable in the settings panel (3–180 day choices), HttpOnly + SameSite=Lax).
  • API protection: Every registered route (/api/* and third-party RPC routes, except /api/auth/* and /login) requires a valid session, otherwise returns 401 or refuses the handshake.
  • "Auth" tab in the settings panel: Injects an "Auth" page into the DSH settings panel with Sign out, Change username, Change password, and a session-lifetime selector. The tab shows a shield-with-check glyph in the nav (upstream lets no registrant pick an icon, so the plugin swaps the default gear client-side).
  • Remote-scenario fixes (LAN/HTTP pitfalls):
    • crypto.randomUUID polyfill — the API is missing in non-secure contexts; without it every RPC fails.
    • Native browser-auth bridge — since dsh 0.1.2 (current baseline 0.2.0-rc.1) upstream ships its own browser authentication (signed dsh-auth-* cookies) and requires the cookie on /api and on index.html with no loopback exemption (even the local browser must first swap a launch-token URL). This plugin mints that cookie for callers that already passed ITS authentication — a valid dsh_sid session, or a genuine loopback request (loopback TCP peer and loopback Host): page navigations pick it up through a single 200 bounce document (a 3xx mint gets replayed on every hop until ERR_TOO_MANY_REDIRECTS, since a cookie set by a redirect response is not sent to that redirect's target) and the login responses hand it out directly, so the printed token URL is never needed. Username/password plus revocable sessions stay the only auth entry point; the upstream cookie merely lets requests through upstream's own gate.

Install

This plugin is a DSH bundle (the dsh.bundle.patch in package.json ships a cordis.patch.yml). After dsh plugin installation the patch layer applies automatically — no manual config editing.

# Option 1: from source
git clone <repo-url>
cd dsh-web-startup-auth
npm install        # install build dependencies (typescript etc.)
npm run build      # compile src/ to lib/ (the runtime loads lib/ artifacts)
dsh plugin --profile web add .

# Option 2: from the npm registry (re-run the same command to upgrade an existing install)
dsh plugin --profile web add dsh-web-startup-auth@latest

dsh plugin forwards to pnpm and requires --profile <name>; add . installs the current directory as a link: dependency.

Version range: this plugin declares support for dsh >=0.1.7-rc.1 and refuses the 0.3.0 line (including its pre-releases). When the running dsh falls outside the range, dsh skips this bundle and prints why — deliberately fail-loud: better not to load at all than to run silently on an unverified dsh minor line. The granularity is the minor line: later versions on the same line (including rc pre-releases) load normally; every new minor line needs adaptation before the range is widened. A profile version exemption can force it on if you insist.

Start:

dsh web --host 0.0.0.0
# pure IPv6 networks (or to cover IPv6 too):
dsh web --host :: --port 8080

Or, if applying the patch manually with --patch ./cordis.patch.yml: dsh --profile web --patch ./cordis.patch.yml --host 0.0.0.0

Usage

  1. Open http://<host-ip>:<port>/ in a browser (from the same machine use http://127.0.0.1:<port>/, which needs no login). IPv6 addresses must be bracketed in URLs: http://[<ipv6-address>]:<port>/. Note: with --host :: the terminal prints only the loopback URL (upstream LAN derivation enumerates IPv4 only), so build LAN IPv6 URLs by hand in the bracketed form. The printed loopback URL is always http://127.0.0.1:<port>/ (upstream hardcodes it); on bindv6only=1 systems a :: bind is an IPv6-only socket and that URL is dead — use http://[::1]:<port>/ locally.
  2. A remote first visit redirects to /login, showing the "set admin credentials" registration form.
  3. After registering you are auto-logged-in and land in the UI; subsequent visits require login.
  4. Sign out / change username / change password / adjust the session lifetime / turn the slider puzzle on or off: open the Settings panel → Auth tab in the UI (there is also a standalone entry; POST /api/auth/logout clears the session cookie).

Credentials and the session secret live in $DSH_HOME/web-auth.json (that is ~/.dsh/web-auth.json when $DSH_HOME is unset, alongside dsh's own data; the DSH_WEB_AUTH_FILE environment variable overrides the whole path):

  • Passwords are stored as scrypt hashes (random salt, 64 bytes); plaintext is never saved.
  • Session cookies are signed with a random key using HMAC-SHA256 to prevent forgery. The lifetime choice (sessionMaxAgeDays, default 14) is persisted in this same file and adjustable in the settings panel; changes only affect freshly issued sessions.
  • Forgot password: on the server machine run dsh --profile web auth-reset for an interactive reset (or dsh --profile web auth-reset --password <new-password> non-interactively). Resetting rotates the session secret and invalidates every issued session.
  • Change username / repair a username containing control characters: dsh --profile web auth-reset --username <new-username> (can be combined with --password). Also rotates the session secret. Usernames are normalized at register/login/change time by stripping C0 control characters (0x00–0x1F) and DEL (0x7F) — if an older version already stored a DEL-polluted username verbatim, this command repairs it.
  • Fallback: delete the credential file ($DSH_HOME/web-auth.json, default ~/.dsh/web-auth.json) and restart to re-register (also invalidates all sessions, but requires a restart).

Index

If you are looking for an out-of-the-box IDE built for the Agent era, check out Omniterm

Security notes

  • This plugin provides authentication but not transport encryption. Over plaintext HTTP, credentials and traffic can be sniffed on the same network — use only on a trusted LAN or put an HTTPS reverse proxy in front.
  • Sessions last 14 days by default, adjustable in the settings panel's "Auth" tab (3/7/14/30/60/90/180 day choices, persisted in the credential file); changes only affect freshly issued sessions — existing ones keep the lifetime they were signed with.
  • Password hashing uses Node's built-in crypto.scryptSync; no third-party dependency.
  • Sessions cannot be revoked server-side: dsh_sid is a self-contained signed cookie; /api/auth/logout only clears it on the browser side. A leaked cookie (e.g. sniffed over plaintext HTTP) cannot be individually revoked within its lifetime (bounded by the session-lifetime choice made in the settings panel). Exceptions: dsh --profile web auth-reset, the "Change password" and "Change username" actions in the settings panel all rotate the session secret, invalidating all sessions at once (after the change the current session is re-issued, so you stay signed in).
  • First-registration window: while no credentials are set, any visitor can register as admin. Complete the first registration before exposing the service to an untrusted network.
  • Login throttling: login failures are rate-limited per client IP — 5 consecutive failures lock the client out for 30 seconds (in-memory only, not persisted); registration requires a password of at least 8 characters. Throttling covers /api/auth/login, /api/auth/change-password, and /api/auth/change-username (a wrong old/current password also counts). For stricter protection, add general rate limiting at your reverse proxy.
  • Two-layer API session gate: every protected route is denied by default at the registration layer (unauthenticated navigations get a 302 to the login page, everything else 401); the shared API additionally carries a session gate mounted on upstream's official connection/request extension point. The two layers insure each other: if an upstream interface change disables one, the other still guarantees that only a revocable session (or a genuine loopback caller) can reach the API — the upstream native cookie, which cannot be revoked on its own for 30 days, never works as an API credential by itself.
  • Credential file permissions: $DSH_HOME/web-auth.json (default ~/.dsh/web-auth.json; password hash + session signing key) is saved with 0600, its directory with 0700; the plugin repairs overly-broad permissions left by older versions at startup.
  • --trusted-host: kept only for CLI compatibility with the stock launcher; it plays no part in this plugin's auth decisions — remote clients always need a valid session; there is no "trusted host skips login".
  • Reverse-proxy deployments (nginx, …): binding dsh to 127.0.0.1 and letting the proxy terminate TLS and forward is supported. The proxy must forward the real Host (nginx does by default via proxy_set_header Host $host;; pass --trusted-host <domain> so DSH's own Host fence accepts it); once authenticated, the plugin mints the upstream native browser cookie under the request's real Host (the public domain), so upstream's gate accepts the request. If the proxy instead hard-codes Host: 127.0.0.1, the plugin reads the request as local and lets all traffic through unauthenticated — do not configure it that way. X-Forwarded-For is never consulted (a client can forge it); trust is decided solely by the TCP peer address and Host.
  • Upstream compatibility (dsh 0.1.7 baseline): From dsh rc.8 through 0.1.1, DSH's frontend decided loopback-ness from the browser address bar hostname (connection.isLoopback), so in a remote browser the settings mirror ran in memory mode and plugin-config cards / the Models page were unusable; this plugin injected a script into the SPA index flipping that flag to a constant true the moment the connection plugin activated. 0.1.2's real cookie authentication gets a remote browser into the UI, but the settings mirror still keys off the same flag — a LAN browser still gets a memory mirror that never reads the host, and the Models (provider directory) section of the settings panel reports "settings are unavailable in this browser". Restoring the old getter override breaks the web boot (A/B verified: 26 frontend plugins stayed pending), so as of 0.1.2 the fix is instead injecting window.__DSH_TRANSPORT__ = { ownsHost: true }: the connection client reads that hook at construction and reports isLoopback as true (api/rpc fields fall back safely when absent, and the cordis service is not rewritten), so every settings surface — Models included — renders normally from both LAN and loopback browsers. Remaining browser-side shims: that transport hook and the crypto.randomUUID polyfill (needed for plaintext-HTTP non-secure contexts).

Development

npm install
npm run typecheck   # tsc --noEmit
npm test            # vitest
npm run build       # tsc -p tsconfig.json + tsdown, output to lib/
  • tsc compiles the node-side source (src/*.ts) and type declarations to lib/ and lib/types/.
  • tsdown bundles the frontend plugin (src/client/index.tsx) into the browser bundle lib/client.js (the window.__ModuleLoader__.load registration format). Rebuild after changing frontend code; a link: install in the profile picks up new artifacts automatically.
  • The @deepseek-ai/dsh-client-* packages the frontend plugin depends on are used only for types and building; at runtime they are provided by DSH's frontend module table.

License

MIT

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →

Community comments

Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.