dsh-net-proxy-plugin

August 30, 2026 · View on GitHub

English | 简体中文

dsh-net-proxy-plugin

A fallback network proxy plugin for DeepSeek Harness (bundle): scans proxies already present on the system (environment variables, Windows system proxy, macOS/Linux system proxy, common local proxy ports), probes overseas reachability (Google 204 and GitHub by default), and when direct access fails while a local proxy works, wires that proxy into the running dsh — all in-process fetch goes through it and HTTPS_PROXY/HTTP_PROXY/NO_PROXY are set so child processes (git, pnpm) launched by the agent use it too.

This is a fallback, not a takeover: with no usable proxy or with direct access working, dsh behaves exactly as if the plugin were not installed; on uninstall the environment variables and the global dispatcher are restored precisely.

Changelog

Latest version only; full history in change-log.en.md.

0.1.0 (2026-08-31)

  • Initial release Plug-and-play: no build step needed (built lib/ is committed).

Features

  • Candidate discovery (deduped by priority): extraProxies config → environment variables (HTTPS_PROXY/HTTP_PROXY/ALL_PROXY and lowercase) → system proxy (Windows WinINet registry, macOS scutil --proxy, Linux GNOME gsettings) → local listening ports (default 7890/7897/7891/10809/10808/8888/8080, both http and socks5 attempted).
  • Reachability probing: direct plus each candidate, one GET per target; an HTTP status below 500 counts as reachable.
  • Auto decision (activation: auto default): activate a proxy when it reaches all targets and at least one target fails directly; activation: always activates the first working proxy regardless. Re-probes every 5 minutes by default (reprobeIntervalMs: 0 disables), so a dropped VPN switches back to direct and a restarted one reactivates; /net-proxy refresh re-probes immediately.
  • Activation scope: all in-process fetch (the undici global dispatcher is replaced by a hostname-routing RoutingDispatcher; hosts in bypassHosts stay direct; covers LLM, web_search, etc.) plus child processes (standard proxy env vars + NO_PROXY).
  • Optional net-proxy provider: the built-in web-fetch-http provider deliberately opens its own connection and bypasses the global fetch proxy; this plugin offers a semantically compatible provider, switchable in one line (see below).
  • /net-proxy command: check current plugin status in the Web UI; /net-proxy refresh re-probes immediately.

Verified on a real machine

During development, on Windows with a VPN system proxy at http://127.0.0.1:51266: the plugin detected direct GitHub connectivity failing and the system proxy reaching every target, then activated automatically; the model's first web_fetch failed on direct and the retry returned 204 through the proxy — exactly the "retry through the local proxy after a timeout" path. The dsh log shows dsh-net-proxy: probe done — direct blocked, working proxies: ..., active: http://127.0.0.1:51266.

A single machine/run record; results depend on the network that day. On other machines use the plugin's own probe output.

Install

Repository: https://github.com/minatoAI/dsh-net-proxy-plugin

The plugin is distributed as a bundle and installed into a profile with dsh plugin (use pnpm dsh instead of dsh when running from a source checkout):

From GitHub. The built lib/ is committed and there is no build script, so no allowBuilds authorization is needed

dsh plugin --profile web add github:minatoAI/dsh-net-proxy-plugin

More stable: pin to a commit so later pushes cannot change what you actually installed

dsh plugin --profile web add github:minatoAI/dsh-net-proxy-plugin#<commit-sha>

Or from a local folder (development; build first)

pnpm install
pnpm run build
dsh plugin --profile web add ./dsh-net-proxy-plugin

After installing, restart dsh (a new bundle takes effect on the next launch):

dsh --profile web

Installation fetches the only runtime dependency undici from npm; @deepseek-ai/cordis, @deepseek-ai/schemastery and @deepseek-ai/dsh-web are peer dependencies resolved from the dsh installation closure — no extra install needed.

Usage

Probing runs automatically at startup. The first probe takes a few seconds (direct + candidates × targets, each up to probeTimeoutMs); one direct request in this window may fail, and the next retry hits the activated proxy.

  • On activation the dsh log emits dsh-net-proxy: activated <url> (system, ...) and a probe done summary.
  • In the Web UI, /net-proxy shows the current state (active proxy, source, activation time, direct reachability); /net-proxy refresh re-probes immediately.

Configuration

Override the net-proxy row in your profile's cordis.patch.yml:

- id: net-proxy
  config:
    targets: [https://www.google.com/generate_204, https://github.com]
    probeTimeoutMs: 5000
    reprobeIntervalMs: 300000
    bypassHosts: [localhost, 127.0.0.1, ::1, api.deepseek.com, "*.deepseek.com"]
    extraProxies: [http://127.0.0.1:7890]
    probePorts: [7890, 7897, 10809]
    activation: auto
FieldDefaultDescription
enabledtrueMaster switch
targetshttps://www.google.com/generate_204, https://github.comReachability probe targets
probeTimeoutMs5000Per-probe timeout
reprobeIntervalMs300000Re-probe interval; 0 disables
bypassHostslocalhost, 127.0.0.1, ::1, api.deepseek.com, *.deepseek.comHosts kept on direct connections while a proxy is active (exact / *.domain wildcard / *)
extraProxies[]Explicit proxies, preferred over discovery
probePorts7890, 7897, 7891, 10809, 10808, 8888, 8080Local ports scanned
activationautoauto: enable only when a target fails directly; always: enable with any working proxy
fetchTimeoutMs30000net-proxy web provider timeout
maxResponseBytes5000000net-proxy web provider response cap
maxRedirects5net-proxy web provider same-origin redirect cap

Routing the web_fetch tool through the proxy

web_fetch (the built-in web-fetch-http provider) deliberately opens its own connection and pins the public IP, bypassing the global fetch proxy. This plugin provides a semantically compatible net-proxy provider (same WEB_FETCH_TIMEOUT/WEB_ABORTED/WEB_REDIRECT_BLOCKED/WEB_BLOCKED_URL/WEB_FETCH_TOO_LARGE error codes; with the proxy, public-IP validation is carried by the proxy you choose). Change one line in your profile's cordis.patch.yml to enable it:

- id: web
  config:
    searchProvider: deepseek-official
    fetchProvider: net-proxy

Without it, web_fetch keeps its original direct path and is not affected by this plugin.

Network & proxy (mainland-China users)

The plugin reads proxies already present on the system and ships no proxy service of its own. When a VPN/proxy is on it is discovered and reused automatically; if the VPN restarts with a new port the next re-probe (5 minutes by default) or /net-proxy refresh heals it. Only use local proxies you trust — the proxy sees and forwards all traffic you send through it; the plugin never alters the Windows system proxy setting and only affects network access of the dsh process and its children.

Uninstall

dsh plugin --profile web remove dsh-net-proxy-plugin

Repository layout

dsh-net-proxy-plugin/
├── package.json       # manifest: "dsh": { "bundle": {"patch": ...} }; built lib/ committed
├── cordis.patch.yml   # composition: single row net-proxy (name: dsh-net-proxy-plugin)
├── src/               # TypeScript source (NodeNext ESM, strict)
│   ├── index.ts       # plugin body: config schema, probe scheduling, activation decision, provider/command registration, lifecycle
│   ├── candidates.ts  # candidate discovery (config→env→system proxy→ports); system-proxy readers (WinINet/scutil/gsettings)
│   ├── probe.ts       # direct/per-proxy reachability probing
│   ├── dispatcher.ts  # undici proxy dispatcher and hostname-routing RoutingDispatcher
│   ├── activate.ts    # ProxyActivation: activate/deactivate/switch-and-restore of global dispatcher and proxy env vars
│   ├── bypass.ts      # host matching (exact / *.domain wildcard) and NO_PROXY value generation
│   └── fetch-provider.ts # optional net-proxy WebFetchProvider (same-origin redirects/size cap/timeout/error codes)
├── lib/               # build output (committed; GitHub install works out of the box)
├── tests/             # vitest: 40 cases (parsing/routing/probing/activation/provider/system-proxy integration)
├── README.md          # 简体中文说明
├── README.en.md       # English README
├── change-log.md      # 完整版本历史(简体中文)
└── change-log.en.md   # Full change history (English)

Development notes

  • Composition follows the dsh convention: a single row net-proxy whose name is the exact package name dsh-net-proxy-plugin.
  • The plugin inject list is empty: probing and activation depend on no service and start immediately; web/commands are optional services registered via ctx.inject, so without them in the composition the related features simply do not register and the loader startup never stalls.
  • Every registration is an effect (ctx.effect/yield returning a disposer); the lifecycle effect clears the re-probe timer and calls activation.dispose() (which restores the global dispatcher and proxy env vars).
  • Relative imports in src/ carry .js suffixes (required by tsc NodeNext emit); tsconfig.json enables strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes.

Tests

pnpm install
pnpm test       # vitest: 40 cases
pnpm run build  # tsc emits lib/
dsh plugin check .   # validate the patch layer and plugin mount (use pnpm dsh from a fork checkout)

Tests use local mock proxies (absolute-form HTTP + CONNECT tunneling) and a real target-server router, covering candidate parsing, host matching/NO_PROXY, RoutingDispatcher, probing/decision, activate/deactivate/switch restore, and the WebFetchProvider contract — 40 cases in total.

Note: dsh plugin check reports unknown inject dependencies: commands — that is a gap in the checker's own known-services list (commands does exist in the composition); it does not affect install or runtime.