**English** | [简体中文](README.md) # dsh-net-proxy-plugin A **fallback network proxy plugin** for DeepSeek Harness ([bundle](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/publish.md)): 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](./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](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/publish.md) 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** ```sh 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 ```sh dsh plugin --profile web add github:minatoAI/dsh-net-proxy-plugin# ``` > Or from a local folder (development; build first) ```sh 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): ```sh 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 (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`: ```yaml - 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 ``` | Field | Default | Description | | --- | --- | --- | | `enabled` | `true` | Master switch | | `targets` | `https://www.google.com/generate_204`, `https://github.com` | Reachability probe targets | | `probeTimeoutMs` | `5000` | Per-probe timeout | | `reprobeIntervalMs` | `300000` | Re-probe interval; `0` disables | | `bypassHosts` | `localhost`, `127.0.0.1`, `::1`, `api.deepseek.com`, `*.deepseek.com` | Hosts kept on direct connections while a proxy is active (exact / `*.domain` wildcard / `*`) | | `extraProxies` | `[]` | Explicit proxies, preferred over discovery | | `probePorts` | `7890, 7897, 7891, 10809, 10808, 8888, 8080` | Local ports scanned | | `activation` | `auto` | `auto`: enable only when a target fails directly; `always`: enable with any working proxy | | `fetchTimeoutMs` | `30000` | `net-proxy` web provider timeout | | `maxResponseBytes` | `5000000` | `net-proxy` web provider response cap | | `maxRedirects` | `5` | `net-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: ```yaml - 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 ```sh 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 ```sh 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.