Files
deepseek-harness/packages/client/connection
Yichen Jiang bf4356cf35 feat(web): let a blank session switch its agent preset
`agentPreset.select` recomposes one session's agent from a different preset.
It is allowed only while the session is blank — once a turn has run, that
history was produced under the preset's tools and swapping them would strand
logged tool calls, so the attempt answers `agent-preset-locked`.

The agent and the session survive; only the preset subtree is swapped. That
was forced by what the host actually owns: api-proxy discards the `AgentHandle`
it creates, and there is no delete RPC, so neither disposing nor recreating the
session was available. Swapping the subtree is also the better answer — the
session id, its workspace attachment, and its projections all stay put.

`recompose` is unmount-then-mount because two compositions cannot coexist: both
would register the same tool names into one layer. So it resolves the new
preset BEFORE tearing anything down (an unknown id is a no-op) and restores
the previous composition when the new one fails to mount, rather than leaving
the agent with no tools at all. Both paths are pinned by test.

Also restores the English half of the `agentPreset.list` README paragraph,
which was lost before the previous commit — and `verify-translation-pairing
--write` recorded the pair as consistent anyway, because it records whatever
state it finds rather than checking the two sides say the same thing.
2026-08-07 00:38:11 +08:00
..

@deepseek-ai/dsh-client-connection

English | 中文

Wire consumer layer: the client plugin's apply mounts ctx.connection (shared api client + current-page loopback state + single-consumer stream-loop starter); the export face carries the wire contract types, the AbstractApiClient seam, and the loop's sink/config types. 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. 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) 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; the protocol contract is api-contracts v3 §3.

/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, the CLI-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. A non-loopback (--host 0.0.0.0) deployment therefore needs its serving authorities trusted: the dsh CLI derives the machine's LAN IP literals itself and its --trusted-host flag declares named ones, so trustedHosts in cordis.yml is for compositions the CLI does not boot. 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/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.