Skip to content
dsh-market Browse plugins GitHub 中文

tr1v3r/dsh-proxy

Hot-switches in-process DSH fetch traffic among direct, manual HTTP(S)/SOCKS5 proxy, and ambient system-proxy modes, with no-proxy rules and optional proxy-environment export for newly spawned child processes.

Stars ★ 2 Category Tools & Capabilities Listed 2026-09-09 npm @tr1v3r/dsh-proxy

Install

Inside DeepSeek Harness, with dsh-market

dsh plugin --profile web add dshmarket

Or from the command line

dsh plugin --profile web add @tr1v3r/dsh-proxy

Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.

README

中文说明见 README.zh.md。

DSH Market

demo: changing proxy modes reroutes outbound requests instantly

@tr1v3r/dsh-proxy is a DeepSeek Harness plugin that routes every in-process outbound request — LLM providers, web_search / web_fetch, streamable-http MCP — through an HTTP(S) CONNECT or SOCKS5 proxy, and lets you flip the proxy on, off, or to another server at runtime, with zero restarts, either from the Web Settings → General → Network proxy control or by editing the dsh-proxy entry in the profile's cordis.patch.yml (hot-reloaded). The demo above shows the routing engine: node scripts/demo.mjs after install.

How it works

DSH and pi-ai issue requests through globalThis.fetch, which reads undici's well-known global dispatcher slot (Symbol.for('undici.globalDispatcher.1')). The plugin owns that slot:

  • http(s):// proxy → EnvHttpProxyAgent (CONNECT tunneling for https)
  • socks5:// proxy → undici's built-in Socks5ProxyAgent (URL credentials supported; socks5h:///socks:// normalize to it; DNS resolves remotely)
  • noProxy rules → both paths route through one RoutingDispatcher, so HTTP and SOCKS share identical matcher semantics (undici-style: bare entries match the host and dot-boundary subdomains; host:port pins a port; * bypasses everything; a leading dot or *. prefix is accepted as a synonym of the bare entry). In manual mode, ambient NO_PROXY/HTTP_PROXY env vars are deliberately ignored by the dispatchers — exported env only steers child processes, so in-process routing is fully determined by the profile entry config. system mode is the opposite: it follows the ambient proxy — HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY env vars, falling back to the macOS System Settings proxy (scutil --proxy) — re-detected each time the section is applied, not continuously polled.

With exportEnv: true (default) the switch also exports HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY into the dsh process, so child processes spawned after the switch (bash-tool curl/git, stdio MCP servers) follow the same proxy. Variables you set yourself at boot are never clobbered, and everything is restored on disable/unload. While loopback bypass is on (the default), the exported NO_PROXY also gains the loopback defaults (localhost,127.0.0.1,::1, merged and deduplicated with your rules); with bypassLoopback: false your list is exported unchanged.

Retired dispatchers close gracefully and are force-destroyed after 30 s, so switching away actually tears down old keep-alive connections. In-flight requests get that same 30-second grace period (RETIRE_DESTROY_MS); a streaming response that is still running ~30 s after you flip the switch is interrupted when the retired dispatcher's sockets are force-destroyed.

Credential security

Proxy URLs with embedded user:pass@ credentials are stored in plaintext on disk — in the profile's cordis.patch.yml and the settings persistence — and are protected only by file permissions. With the default exportEnv: true, the credentials are also propagated into the dsh process as HTTP(S)_PROXY env vars, so every child process spawned afterwards carries them (ps -E on Linux/macOS or /proc/<PID>/environ can reveal them to the same user; other users generally need root or comparable privileges to read them, depending on platform permission settings). No new config option is introduced for this; if your credentials are sensitive, prefer pointing dsh-proxy at a local, unauthenticated proxy entry (e.g. http://127.0.0.1:7890 in front of an authenticated upstream) instead of embedding user:pass@ in the URL.

Install

In the target profile directory (~/.config/dsh/profiles/<name>/):

  1. Add the dependency and bundle in package.json:

    {
      "dependencies": {
        "@tr1v3r/dsh-proxy": "^0.2.4"
      },
      "dsh": {
        "profile": {
          "bundles": ["@deepseek-ai/dsh-base", "@tr1v3r/dsh-proxy"]
        }
      }
    }
    

    (Merge the bundle into your existing dsh.profile.bundles list.)

  2. Install:

    dsh plugin --profile <name> install --no-frozen-lockfile
    
  3. Restart dsh once to mount the plugin; afterwards never again — switching happens through settings.

Use

In the Web profile, use the icon-only Proxy status control in the sidebar footer (above Settings) to check the selected mode on hover/focus or in its menu and switch between Direct, Follow system, and Manual proxy without leaving the main screen. The menu uses the same settings namespace and updates when the profile patch changes externally. The tooltip shows the manual endpoint with credentials masked; Follow system reflects the selected mode, not a guarantee that the host detected a usable proxy (consult DSH logs for the effective route). A missing manual URL cannot be activated from the quick menu. To edit the URL, bypass hosts, or child-process export, open Settings → General → Network proxy.

In Settings → General → Network proxy, select Direct, Follow system, or Manual proxy from the dropdown; mode changes apply immediately, without an Apply button. In Manual mode, enter an HTTP(S)/SOCKS5 URL and bypass hosts (one per line); text fields save on blur, while the child-process env switch saves on change. Invalid URLs are not saved. The UI writes the same dsh-proxy profile entry. Editing the profile patch remains supported and refreshes the UI; a revision fence prevents a stale edit from silently overwriting an external change.

Manual proxy settings in the DSH Web interface

The selector offers all three routing modes:

Network proxy mode menu: Direct, Follow system, Manual proxy

Alternatively, add this override to ~/.config/dsh/profiles/<name>/cordis.patch.yml (hot-reloaded, no restart). If the file already contains entries, append this item to the YAML list, or edit the existing dsh-proxy item. The mode key picks direct, system, or manual:

- id: dsh-proxy
  config:
    mode: manual                           # direct | system | manual
    proxy: socks5://127.0.0.1:1080         # manual only — http://…, https://…,
                                           # socks5://user:pass@host:1080, socks5h://…
    noProxy:                               # manual only — optional bypass list
      - localhost
      - .internal.example
      - registry.corp:443
    bypassLoopback: true                   # manual + system — loopback stays direct
                                           # (false to proxy it)
    exportEnv: true                        # manual only — also set HTTP(S)_PROXY for children
mode behavior
direct No proxy — everything goes out directly (same as the old enabled: false).
system Follow the host's proxy, detected each time the section is applied: HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY env vars everywhere, falling back to the macOS System Settings network proxy (scutil --proxy) when env is unset. It re-detects on settings save, not continuously; Windows registry, Linux-desktop and PAC are not yet covered. proxy/noProxy/exportEnv are ignored.
manual Route through the proxy URL with the optional noProxy bypass list (same as the old enabled: true).

enabled: true/false still works as a deprecated alias for manual/direct when mode is omitted:

- id: dsh-proxy
  config:
    enabled: true                          # ≡ mode: manual
    proxy: http://127.0.0.1:7890

Every save re-routes immediately. The plugin logs each switch:

dsh-proxy: routing global fetch via socks5://***@127.0.0.1:1080, noProxy 3 rule(s)
dsh-proxy: following system proxy (http://127.0.0.1:7890, noProxy 3 rule(s))
dsh-proxy: direct (mode: direct)

(Userinfo in the proxy URL is redacted in logs. system mode follows the ambient env/OS proxy, so it never writes those env vars itself.)

What is covered / not covered

Traffic Routed?
LLM providers via pi-ai (zai-coding-cn, custom openai-compatible routes, …) ✅
dsh-llm-deepseek (deepseek-official) ✅
web_search / web_fetch ✅
streamable-http MCP servers ✅
stdio MCP servers, bash-tool subprocesses (curl, git, …) ✅ via exported env, for processes spawned after the switch
Loopback destinations (localhost, 127.0.0.0/8, ::1, 0.0.0.0) ❌ direct by default; set bypassLoopback: false to proxy them
pi-ai Bedrock route ⚠️ AWS SDK manages its own proxying (HTTPS_PROXY env is honored there)
Built-in browser host / browser downloads ❌ separate process, configure the browser itself

Also note: child processes already running when you flip the switch keep the env they were spawned with; undici's SOCKS5 agent is currently marked experimental upstream.

Development

npm install
npm test                      # unit + local e2e: HTTP proxy, SOCKS5, noProxy, hot-switch, env
node scripts/boot-probe.mjs   # boots a real DSH tree and switches through Settings

The boot probe needs a DSH >= 0.1.7-rc.1 installation (it uses the createRuntimeResolution / PluginPackages exports that older versions lack — with dsh 0.1.5.x it fails with TypeError: createRuntimeResolution is not a function). You don't have to upgrade your global install: point DSH_ROOT at any matching package tree, for example one installed into a scratch directory:

npm install --prefix /tmp/dsh-probe-root @deepseek-ai/dsh@0.1.7-rc.1
DSH_ROOT=/tmp/dsh-probe-root node scripts/boot-probe.mjs

Run both lines from this repository's root. The scratch install hoists the dependencies to /tmp/dsh-probe-root/node_modules, so DSH_ROOT points at the install anchor directory /tmp/dsh-probe-root itself — not at the package directory.

Without DSH_ROOT, the probe resolves the dsh found on PATH and expects that installation to already satisfy the version requirement.

License

MIT © tr1v3r

Content from the project README on GitHub ↗

Comments

Comments live in GitHub Discussions. Sign in with GitHub to post or react.