Back to home

aashizpoudel

dsh-remote-trust

DSH plugin: open privileged /api methods (settings, credentials, model discovery) to remote trusted hosts

Stars
0
Language
JavaScript
Created
Aug 16, 2026
Updated
Aug 16, 2026

Introduction

@crckss/dsh-remote-trust

Open DSH's privileged /api methods to remote trusted hosts, so a browser visiting the Web UI through a reverse proxy (or from another machine on your network) can configure the harness — Settings → Models, provider and credential management — instead of getting 403 forbidden.

# ~/.dsh/profiles/web/cordis.patch.yml
- insert:
    - id: remote-trust
      name: "@crckss/dsh-remote-trust"
      config:
        hosts:
          - dsh.example.com

Why this exists

DeepSeek Harness binds its Web server to loopback for safety (--host 0.0.0.0 is refused outright) and its /api browser-trust fence has two layers:

  1. A general fence — the request's Host must be loopback or a --trusted-host authority, with Origin and sec-fetch-site checks.
  2. A privileged-method pinsettings.*, credentials.*, agentPreset.*, and llm.discoverModels are additionally FORCED to loopback with an empty trust list, even when --trusted-host names your domain.

That pin is hardcoded in @deepseek-ai/dsh-client-connection, so a deployment behind Traefik/Caddy/nginx can browse sessions but can never open the settings plane. This plugin removes the pin for endpoints you declare, without touching node_modules.

How it works

The webserver's route table is checked exact-first, then longest-prefix. The built-in /api route is a prefix route, so this plugin registers exact routes for each privileged endpoint (/api/settings.describe, …). Those exact routes win, run the same browser-trust fence (Host + Origin + sec-fetch-site) against loopback plus your configured hosts, and then forward the request to apiProxy exactly like the built-in bridge would.

The two host-native methods (host.pickDirectory, host.openPath) are not opened by default because they drive native dialogs on the server machine.

Install

Manually (local profile)

# from your DSH profile directory
cd "${DSH_HOME:-~/.dsh}/profiles/web"   # or profiles/tui, …

# pnpm must be on PATH (dsh plugin forwards to pnpm)
dsh plugin --profile web add @crckss/dsh-remote-trust
# or, from a local checkout:
dsh plugin --profile web add /path/to/dsh-remote-trust

From npm

dsh plugin --profile web add @crckss/dsh-remote-trust

Configuration

- insert:
    - id: remote-trust
      name: "@crckss/dsh-remote-trust"
      config:
        # Authorities allowed to call privileged methods.
        # Port-less host matches any port; host:port matches exactly.
        hosts:
          - dsh.example.com
          - 100.34.48.118
        # Optional: open privileged methods beyond the defaults
        # (these are host-native actions, opt-in).
        extraEndpoints:
          - host.pickDirectory
        # Optional: request body ceiling for the forwarded bridge.
        maxRequestBodyBytes: 4194304

Trusted authorities are merged from two places:

  • config.hosts — explicit entries in the patch layer;
  • ctx.webStartup.trustedHosts — whatever the invocation declared with --trusted-host, plus LAN IP literals derived from the bind (when bound to all interfaces).

So the plugin follows the deployment's existing trust declaration: if you already run dsh web --trusted-host dsh.example.com, the plugin picks that up even with an empty hosts list.

Opened by default

agentPreset.read          agentPreset.copy
agentPreset.openDocument  agentPreset.remove
settings.describe         settings.openDocument
settings.update           settings.replace
settings.mutate
credentials.describe      credentials.set
credentials.unset
llm.discoverModels

host.pickDirectory and host.openPath stay loopback-only unless listed in extraEndpoints.

Security model

The plugin reuses the harness's own browser-trust logic — it does not add a looser check:

  • Host must be loopback or a configured authority (DNS-rebinding gate);
  • Origin, when a browser sends one, must match the Host;
  • sec-fetch-site: cross-site is always refused.

Nothing becomes unauthenticated: the fence is not authentication, and it never was. What changes is that the config plane stops being loopback-only and becomes reachable from the authorities you already trust with --trusted-host.

Do not expose the DSH port (3080) or the whole machine through a public proxy unless you add your own authentication. The fence protects against rebinding and cross-site requests, not against a stranger who can reach the port.

Verify

BODY='{"type":"client-request","rpcId":"v","method":"settings.describe","payload":{}}'
# remote host -> should be 200 (was 403 without the plugin)
curl -i -H 'Host: dsh.example.com' -H 'Content-Type: application/json' \
  -d "$BODY" http://127.0.0.1:3080/api/settings.describe
# cross-site / attacker host -> must stay 403
curl -i -H 'Host: dsh.example.com' -H 'Origin: https://evil.example' \
  -H 'sec-fetch-site: cross-site' -H 'Content-Type: application/json' \
  -d "$BODY" http://127.0.0.1:3080/api/settings.describe
curl -i -H 'Host: evil.example' -H 'Content-Type: application/json' \
  -d "$BODY" http://127.0.0.1:3080/api/settings.describe

Known limitations

  • Tracks the harness release. The pin lives in @deepseek-ai/dsh-client-connection; this plugin pins @deepseek-ai/dsh-host-apiproxy to a matching version so the forwarded bridge stays compatible. After upgrading dsh, bump that dependency.
  • It opens, it does not delete. Deleting a privileged method still needs a cordis.yml composition change; the settings seam's merge semantics apply.
  • No auth added. Add your own if the endpoint can be reached by people you do not trust.

License

MIT