返回目录
其他 待识别

dsh-proxy

tr1v3r/dsh-proxy

Runtime-switchable outbound proxy plugin for the DeepSeek Harness — hot-reload HTTP(S)/SOCKS5 routing via one settings section

Stars
2
Forks
1
Issues
2
更新
4 天前

PROJECT TOPICS

项目标签

PROJECT README

README

dsh-proxy — runtime-switchable outbound proxy for DSH

中文说明见 README.zh.md。

npm license DSH Market

Web GUI demo: sidebar proxy icon, quick mode switcher, and applied-route status

@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 GIF above uses real screenshots from an isolated DSH Web profile: the sidebar proxy icon, Direct / Follow system / Manual quick switcher, and Settings → General → Network proxy with the host-applied route snapshot. The endpoint is illustrative; the snapshot is not a connectivity test. For the separate terminal routing-engine demo, run 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.5"
      },
      "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.)

Applied route snapshot

The General settings row can read the host's last applied default route through an optional, authenticated Connection Fetch route. It shows the selected mode, source, separately redacted HTTP/HTTPS endpoints, bypass policy, generation and stable apply/fallback code. Refresh explicitly after a save if the host has not applied that revision yet. Unsupported/disconnected hosts show “unavailable”; normal settings and quick switching still work without this capability.

This is not a connectivity/health test or a per-request trace. direct restores the original dispatcher, which another plugin may have configured; it does not guarantee physical direct access. system reports the last installed result, not a fresh OS/environment detection. NO_PROXY and loopback policy may bypass individual requests. No arbitrary URL probe, provider request, credentials or full proxy URL is sent by this status feature. Provider-aware diagnostics remain deferred until adapters expose an explicit effective-target/security contract.

When exporting NO_PROXY with loopback bypass enabled, only identical trimmed entries are deduplicated, preserving user spelling and order and appending any missing standard defaults: localhost, 127.0.0.1, ::1. Case, trailing-dot and IPv6-bracket variants remain separate; for example, 127.0.0.1. must not remove 127.0.0.1, nor [::1] remove ::1. External clients do not necessarily share the in-process hostname matching rules (curl treats these IP variants differently). No cross-client equivalence is assumed for localhost variants either. Suffix/wildcard/port rules and other addresses are not canonicalized. bypassLoopback: false still exports the literal user list without defaults or deduplication; each child client interprets that list itself. The broader in-process loopback detection is unchanged.

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
node scripts/boot-probe.mjs --web # also tests authenticated status GET + auth/Origin fences

The boot probe checks actual runtime capabilities before listening, creating its throwaway home or booting. DSH >= 0.1.7-rc.1 is the verified reference, not a version gate: the install anchor, imports and required exports (including createRuntimeResolution / PluginPackages) must be present. Missing roots, modules and capabilities produce actionable preflight errors instead of a late TypeError. You need not upgrade your global install; use a scratch directory:

ROOT=$(mktemp -d)
npm install --prefix "$ROOT" @deepseek-ai/dsh@0.1.7-rc.1
DSH_ROOT="$ROOT" node scripts/boot-probe.mjs

Run these commands from this repository's root. DSH_ROOT names the install anchor containing package.json; dependency resolution supports hoisted scratch installs as well as package-local dependencies. Without it, the probe resolves the real dsh executable on PATH and preflights that installation.

PROBE_HOME, if supplied, must be an existing parent directory. The probe owns and cleans only a unique temporary child, never that parent or its contents. Inherited proxy variables are cleared in the probe process. All routing checks use local servers; no provider/model request is made.

License

MIT © tr1v3r

CLASSIFICATION EVIDENCE

分类依据

项目类型待识别
功能分类其他
规则置信度低

系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。