# dsh-remote-trust
[中文](README.md) · [English](README-en.md)
Makes a **reverse-proxied (non-loopback)** dsh page count as a trusted host, so the
**Settings surface** actually works — undoing the upstream "non-loopback pages keep the
client policy" design gate.
## Symptom
The page loads and chat works, but **Settings → Models** reports:
```
Failed to load the provider directory: settings are unavailable in this browser
```
Accessing `http://127.0.0.1:3080` on the host itself is fine.
## Root cause (an intentional gate, not a bug)
`@deepseek-ai/dsh-client-connection` decides whether the page is trusted:
```js
isLoopback = transport?.ownsHost === true
|| pageLocation === void 0
|| isLoopbackHostname(pageLocation.hostname) // localhost / [::1] / 127.0.0.0/8
```
`@deepseek-ai/dsh-client-ui-settings` uses that decision to pick the settings mirror's
persistence mode:
```js
const persistence = ctx.remote.$host.isLoopback ? "host" : "memory";
```
A non-loopback page gets `"memory"`, where the mirror's `ensure()` / `load()` return
immediately and **never issue `settings.describe`**; `dsh-client-ui-settings-models` then
sees `view === undefined` and can only render that misleading fallback.
The server does **not** block anything here: a domain declared through `--trusted-host`
already passes the `/api/*` trust fence (this version has no loopback pin on privileged
methods) — only the browser-side decision is missing.
## How it works
It uses the official index-transform API (`ctx.webServer.tapIndex`) to inject one line
into index.html:
```html
```
`ownsHost` is the official escape hatch `dsh-client-connection` reserves for
"shells that assemble their own transport": with it declared, `isLoopback` becomes true,
the settings mirror switches to `host` persistence and really reads.
- No DSH source is modified, no `server.emit` override, no dependencies.
- The injection is idempotent (returns the input unchanged when already declared) and
returns the input unchanged when `` is missing.
- Uninstall is clean: the tap disposer is registered on `ctx.effect`.
## Install
```bash
dsh plugin --profile web add dsh-remote-trust
```
Restart dsh (a profile without `patchReload: live` always needs a restart).
## Verify
Browser DevTools console:
```js
globalThis.__DSH_TRANSPORT__ // expect { ownsHost: true }
```
Then open **Settings → Models**: the provider directory should load, and the Network
panel should show `POST /api/settings/describe → 200`.
## ⚠️ Security boundary (please read)
This plugin **deliberately relaxes a design gate**, so enable it only where you
understand the consequences:
- Declaring `ownsHost` makes **that page** count as a trusted local host, which also
unlocks other loopback-only client surfaces (e.g. "open the config file", native
directory pickers).
- The page therefore **must** stay protected by dsh's own token / browser session
cookie; for public deployments add a second factor (nginx Basic Auth, an IP allowlist,
or an authenticating gateway).
- It does **not** change the server-side trust fence: `/api/*` is still
"loopback ∪ `--trusted-host`", so you still must start dsh web with
`--trusted-host `.
- Only enable it for deployments you can reach yourself and have protected by other means.
## Compatibility
- Verified on **dsh `0.2.0-rc.2`** (containerised `dsh web` behind a host nginx
reverse proxy on a domain).
- Depends on two upstream internals: `ctx.webServer.tapIndex()` (missing → **fails soft**:
one warning, nothing injected, host unaffected) and the `__DSH_TRANSPORT__.ownsHost`
escape hatch.
- If upstream changes the latter, this plugin stops working — the symptom is that
settings message coming back.
## Uninstall
```bash
dsh plugin --profile web remove dsh-remote-trust
```
After a restart, index.html is served exactly as before.
## License
MIT