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.
This commit is contained in:
ZiyaZhang
2026-08-01 02:17:25 -07:00
parent 1082518520
commit 59bfe77fb8
37 files changed
+469 -197

No files matched your search

@@ -1,6 +1,5 @@
/** Test-owned workspaces face: the renderer standard-kit observable plus recorded actions. */
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import { workspaceFileSegments, workspaceFileUrl } from '@deepseek-ai/dsh-host-apiproxy/api'
import type {
DirectoryListing, IWorkspaces, SessionId, SnapshotStore, WorkspaceId, WorkspaceListState, WorkspaceView,
} from '@deepseek-ai/dsh-client-runtime/client'
@@ -99,21 +98,6 @@ export class TestWorkspaces implements IWorkspaces {
await (this.stubs.get('openPath')?.(path) as Promise<void> | undefined)
}
/**
* Workspace-file URL (recorded). Runs the production path derivation so a
* feature test sees the real in/outside-workspace split; stub to force either.
* @param sessionId - the session whose cwd anchors the path.
* @param cwd - that session's working directory.
* @param path - the path a tool reported.
* @returns the origin-relative URL, or undefined outside the workspace.
*/
fileUrl(sessionId: SessionId, cwd: string | undefined, path: string): string | undefined {
this.calls.push({ method: 'fileUrl', args: [sessionId, cwd, path] })
const stub = this.stubs.get('fileUrl')
if (stub !== undefined) return stub(sessionId, cwd, path) as string | undefined
const segments = workspaceFileSegments(cwd, path)
return segments === undefined ? undefined : workspaceFileUrl(sessionId, segments)
}
/**
* Directory picker (recorded). The default cancels (null); stub to select.