Files
deepseek-harness/packages/client/connection
ZiyaZhang 59bfe77fb8 feat(web): serve workspace files from their own origin
A sandbox header bought isolation by taking the document's origin away, and
measuring that cost decided against it: the reported artifact throws
SecurityError on load, and because an uncaught exception aborts the rest of
its <script>, every listener declared after that line — theme toggle, mobile
menu, model tabs — never binds. Two of the four artifacts in the reporting
user's workspace were dead pages under it, and they still looked right.

A second listener on the API's host, answering /f and nothing else, is the
same boundary without the amputation: cross-origin to /api (refused by the
Origin fence and by CORS), same-origin with itself (localStorage, cookies and
fetch all work). Its port is published into the index page; the browser half
reads it to address previews, and its absence — the keyless fixture lane — is
what makes a file row fall back to the Host opener instead of a dead tab.

fileUrl moves from IWorkspaces to ConnectionHandle: the transport owns both
the listener that serves the bytes and the port that addresses it.
2026-08-01 02:17:25 -07:00
..

@deepseek-ai/dsh-client-connection

English | 中文

Wire consumer layer: the client plugin's apply mounts ctx.connection (shared api client + single-consumer stream-loop starter); the export face carries the wire contract types, the AbstractApiClient seam, and the loop's sink/config types. The node half owns both browser-facing prefixes — /api for RPC and /f for workspace-file reads — behind one trust fence. The /api route pins the privileged method set (host.pickDirectory, host.openPath, and the whole configuration plane — settings.describe/update/replace/mutate and credentials.describe/set/unset, reads included, since describing returns the exposed configuration 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 subclasses (WebApiClient/FixtureApiClient), the ConnectionController loop, and the fixture data source are package-internal — apply selects and drives them; tests reach them via src. Contract: api-contracts v3 §3.

/api browser-trust fence

The node half guards every request under /api before bridging (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 requests without browser markers: over plain HTTP a browser attaches neither Origin nor Fetch-Metadata to reads (EventSource, images, navigations — those headers go only to trustworthy destinations), so an unmarked request may still be a rebound browser read with a readable response, and Host is the one header rebinding cannot forge; 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. Failures answer plain 403 before any RPC dispatch. 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 deliberately not an authentication layer — reachability policy stays with the webserver binding, and auth remains deferred work. Decision record: the api browser-trust boundary Agent Note.

/f workspace-file reads

The node half also serves one file at a time out of a Session's workspace under /f/<sessionId>/<segments…>, so a produced deliverable is reachable from the page that reported it — an http page cannot follow a file:// link, and a browser that is not on the Host machine has no such path anyway. The segments ride the URL rather than a query parameter so a served document's relative references resolve to its siblings. The request names a Session and the gateway names that Session's directory (ApiProxy.workspaceRootOf, which answers from a live agent's header or the persistence store and never resumes an agent to serve a file); this package reads the authority rather than the core services, because holding their host-side Context declarations would merge them over the browser runtime's own. The URL shape itself lives with the other browser-importable contract surfaces, in @deepseek-ai/dsh-host-apiproxy/api, so the browser half that builds a URL and this half that parses one share a single encoding decision. Both the cwd and the resolved target go through realpath before comparison, so a symlink inside the workspace pointing out of it is refused by its target rather than its name; traversal spellings are refused earlier still, at parse time, before any filesystem call. Reads stream (no request buffers a file), answer GET/HEAD only, and carry nosniff with no-store. Extensions outside the served content-type table are typed text/plain rather than offered as a download, because a workspace read is a request to see a file.

Workspace files are served from their own port, and therefore their own origin. That port is the isolation: a workspace file is not necessarily agent-authored — a read row makes every file in a cloned repository openable — so an active document served beside /api would have its script pass the browser-trust fence into every method, the loopback-pinned settings and credential plane included. A different origin closes that without touching the document: a preview keeps localStorage, cookies, and its own fetch, while a call to the API is cross-origin and refused twice over — by the fence's Origin check and by CORS. The alternative, Content-Security-Policy: sandbox, buys the same boundary by taking the document's origin away entirely, which measurably breaks the pages this route exists to show (a page that reads localStorage throws on load, and every listener declared after that line in the same script never binds). The listener binds the same host as the API, so a client that can reach the app can reach its previews; it answers the /f prefix and nothing else — no index, no SPA fallback, no API — and its port is published into the index page as window.__DSH_FILES_PORT__, which the browser half reads to address it. The same trust fence gates it, so a trustedHosts deployment serves workspace files exactly where it serves ordinary reads.

Keyless fixture

A fixture page is served by no host, so no workspace-file port is published into it and ConnectionHandle.fileUrl answers undefined — a file-path row falls back to the Host opener rather than opening a dead tab.

Any fixture query parameter selects the in-memory carrier. fixture=empty starts with no Workspace or Session; fixturePrompt=reject rejects prompts before acceptance; fixtureAttach=fail publishes a Session but rejects its Workspace attachment; fixtureSessionCreate=drop-response publishes and frames a Session before dropping the create response; and fixtureFrames=workspace-first reverses the default session-first create-frame order. Workspace creation by name/path and caller-preallocated SessionIds remain deterministic enough for assembled Web tests to reconcile list and frame arrival. Fixture content search preserves the production-facing unicode61-style case, diacritic, and token-phrase behavior and returns a match-centered snippet of at most 120 Unicode code points.

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's implicit resume is arguable — opening history on an unattached session pulls an agent up host-side; the pure-persistence-read alternative is recorded in the rt-core reconciliation ledger, unchanged in P-I. This package's consumers see it as latency on first open.
  • ToolEventView/ToolCallView/ToolResultView re-exports are scheduled for removal — they fall when the toolview migration deletes the host viewFor line (presentation belongs to the client); the fixture keeps a local viewFor mirror until then.