Overview
dsh-client-connection
Source-level overviewWire consumer layer: the client plugin's apply mounts ctx.connection (shared api client + current-page loopback state + observable generation-scoped hostDescription + single-consumer stream-loop starter); the export face carries the wire contract types, the AbstractApiClient abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact host.describe value before onConnected; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for events.mux and events.host; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single /api route and its Fetch bridge; a registered Typert interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the /api Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived ctx.connection.isLoopback state. The node half's /api route pins the privileged method set (host.pickDirectory, host.openPath, and the whole configuration plane — settings.describe/openDocument/update/replace/mutate and credentials.describe/set/unset; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, agentPreset.read/copy/openDocument/remove, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); agentPreset.list and agentPreset.select stay out — the roster carries only ids and trust, and choosing a preset grants nothing session.create's own agentPreset did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared trustedHosts authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the WebSocket downlink carrier Agent Note.Collapse technical overview
ctx.connection (shared api client + current-page loopback state + observable generation-scoped hostDescription + single-consumer stream-loop starter); the export face carries the wire contract types, the AbstractApiClient abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact host.describe value before onConnected; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for events.mux and events.host; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single /api route and its Fetch bridge; a registered Typert interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the /api Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived ctx.connection.isLoopback state. The node half's /api route pins the privileged method set (host.pickDirectory, host.openPath, and the whole configuration plane — settings.describe/openDocument/update/replace/mutate and credentials.describe/set/unset; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, agentPreset.read/copy/openDocument/remove, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); agentPreset.list and agentPreset.select stay out — the roster carries only ids and trust, and choosing a preset grants nothing session.create's own agentPreset did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared trustedHosts authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the WebSocket downlink carrier Agent Note.This is an atomic module already shipped with Harness, not a standalone profile layer.
Capabilities
What it contributes
README / EN
Package documentation
@deepseek-ai/dsh-client-connection
English | 中文
Wire consumer layer: the client plugin's apply mounts ctx.connection (shared api client + current-page loopback state + observable generation-scoped hostDescription + single-consumer stream-loop starter); the export face carries the wire contract types, the AbstractApiClient abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact host.describe value before onConnected; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for events.mux and events.host; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single /api route and its Fetch bridge; a registered Typert interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the /api Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived ctx.connection.isLoopback state. The node half's /api route pins the privileged method set (host.pickDirectory, host.openPath, and the whole configuration plane — settings.describe/openDocument/update/replace/mutate and credentials.describe/set/unset; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, agentPreset.read/copy/openDocument/remove, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); agentPreset.list and agentPreset.select stay out — the roster carries only ids and trust, and choosing a preset grants nothing session.create's own agentPreset did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared trustedHosts authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the WebSocket downlink carrier Agent Note.
/api browser-trust fence
The node half guards every entry under /api before bridging or upgrading (src/api-request-trust.ts). Every request — browser-marked or not — must present a Host that is a loopback authority or matches a trustedHosts entry: exact on host:port entries, any port on port-less entries, both sides compared through WHATWG normalization (DNS-rebinding defense). There is deliberately no shortcut for unmarked HTTP requests: over plain HTTP a browser attaches neither Origin nor Fetch-Metadata to image and navigation reads, so an unmarked request may still be a rebound browser read with a readable response, and Host is the one header rebinding cannot forge; a browser WebSocket handshake carries Origin and passes the same comparison. Non-browser clients pass the same fence via loopback, deployment-derived LAN IP literals, or a declared authority. When markers are present, an attached Origin must equal the Host authority, and an explicit sec-fetch-site: cross-site marker is refused. A trustedHosts entry that is not a bare, canonical host[:port] authority — one WHATWG parsing reads back exactly as written — fails the plugin load loudly: parsing would otherwise quietly authorize the hostname inside harness.internal/path, or broaden a dangling-colon or zero-padded port to an any-port grant. HTTP failures answer plain 403 before any RPC dispatch; upgrade failures reject the handshake before any event stream starts. Non-loopback compositions must trust their serving authorities explicitly: the Web runtime derives LAN IP literals from an all-interfaces server config, while trustedHosts in cordis.yml and the CLI's --trusted-host flag declare named authorities. dsh web --host 0.0.0.0 is intentionally unsupported until remote access has an authentication layer. The fence is a reachability policy, not authentication; the Web carrier provides no authentication layer. Decision record: the api browser-trust boundary Agent Note.
/api WebSocket downlinks
/api/events.mux and /api/events.host each accept a WebSocket upgrade and send only the corresponding ServerRequest text messages to the browser; the client sends no application data over these sockets. If either socket ends, the current connection generation fails and rebuilds both streams; readiness still requires both sockets to be open and the host.describe HTTP call to succeed. Host teardown terminates both sockets, aborts their sources, and waits for source cleanup before returning. Ordinary network GETs to these paths return 426 with no SSE fallback; toFetchHandler's SSE codec serves only the isomorphic in-process carrier.
Model Experience
None, as the wire consumer layer moves already-composed messages between browser and host; nothing here reaches a model request.
KV Cache effect
None; this package neither assembles nor sends a provider request.
Known Limitations and Deferred Work
- History resumes an unattached session — opening history may create the host-side agent and add latency to the first open; there is no persistence-only read path.
- The
/apibridge buffers each request body in memory —maxRequestBodyBytes(default 160 MiB, sized for the default 100 MiB aggregate image limit after base64 expansion plus envelope headroom) is therefore also the per-request resident bound; a streaming body path would be needed to lower it without shrinking the image limits.
LIMITATIONS
Known limitations
- **History resumes an unattached session** — opening history may create the host-side agent and add latency to the first open; there is no persistence-only read path. - **The `/api` bridge buffers each request body in memory** — `maxRequestBodyBytes` (default 160 MiB, sized for the default 100 MiB aggregate image limit after base64 expansion plus envelope headroom) is therefore also the per-request resident bound; a streaming body path would be needed to lower it without shrinking the image limits.
