Five findings from the #939 review, each reproduced before being fixed. **Configuration reads are as privileged as writes.** `settings.describe` returns every exposed namespace's configuration and `credentials.describe` reports whether an arbitrary environment-variable name is configured and from where — reconnaissance no anonymous caller should have. Both join PRIVILEGED_METHODS, so the whole configuration plane is loopback-only until real authentication exists; `trustedHosts` was never authentication. The model catalog stays reachable: it carries no endpoints or key state, and a LAN client's model picker legitimately needs it. Asserted over a real HTTP server, because the Host header a browser actually sends is what decides this. **The proxy serves only namespaces a registered model provider addresses.** The settings seam is general — any plugin may register one — but the Web configuration plane is the model-provider surface. Without the gate, every future `settings.register()` would silently become remotely readable and writable configuration. An unregistered namespace and an unexposed one answer identically, so no caller can enumerate the registry one probe at a time. **Path-addressed writes replace the redacted-document rebuild.** The editor reads the REDACTED descriptor, so rebuilding a section from it and replacing wholesale deleted every literal secret the wire never returned — reproduced as `{baseURL, reasoning}` in, stored `apiKey` gone out. `settings.mutate` applies set/unset ops to the section as it stands at the front of the seam's write queue, and the client names only fields it can see, so an unseen secret is untouched by construction rather than by care. P2s in the same pass: `llm/adapters-updated` now contains async listener rejections (an uncontained one escaped as unhandledRejection, contradicting the documented "observer failures are contained"); llm-deepseek's retry-policy swap uses the atomic `registration.replace` instead of dispose-then-register, which published `[]` then `["deepseek-official"]` so an observer saw the provider disappear and come back; and a transport rejection no longer strands the page in `loading` or a card in `busy`, with removal failures surfaced on the page banner instead of swallowed.
host/ — web-GUI host half
English | 中文
The host side of the dsh web GUI: the API gateway every client shape shares, and the plain HTTP server it rides on. The browser side lives in client/; the composed application is apps/cli serving apps/web. All product packages.
| Package | Role | ctx key |
|---|---|---|
apiproxy/ |
The shared API gateway: the zero-Node TS wire contract (src/api/), the fetch carrier pair (toFetchHandler host-side, AbstractApiClient client-side), and the host implementation over ctx.agents/ctx.workspace |
ctx.apiProxy |
webserver/ |
Plain HTTP route-registration carrier: node:http server listening on activation; routes register as named exact/prefix handlers |
ctx.httpServer |
directory-picker/ |
Workspace-directory picking seam: discriminated native/browse capability the gateway's picker RPCs delegate to |
ctx.directoryPicker |
directory-picker-native/ |
Dual-face native interaction: OS-chooser backend (osascript / PowerShell / Zenity+KDialog, host-display only) + the browser half filling ui-workspace's directory-flow slots | (registers ctx.directoryPicker) |
directory-picker-browse/ |
Dual-face browse interaction: listing/creation primitives over Node stdlib (remote-capable) + the browser half rendering the in-app Select Workspace Directory dialog | (registers ctx.directoryPicker) |
apiproxy is transport-agnostic by design — it registers no routes; carriers wrap ctx.apiProxy themselves. The HTTP carrier route (with its /api browser-trust fence) is mounted by client/connection's node half, which is why that package lives in the client group: it owns both ends of the wire.