The host platform is the browser-facing half of DeepSeek Harness: the process that binds a loopback node:http server, registers API routes on it, serves the built Web-shell dist, exposes a read-only view of which plugins are loaded, and lets an operator choose a workspace directory. Everything here is a Cordis plugin — the "host" is not a monolithic binary but a composition under packages/host/*, wired together by a leaf cordis.yml. It stands in deliberate contrast to the client plane (the browser bundles under packages/client/*): host packages run Node and own sockets, files, and processes; client packages run in the browser.
| Package | Role | ctx key / seam |
|---|---|---|
packages/host/webserver | node:http server; HTTP + upgrade + fallback route registries | ctx.webServer |
packages/host/frontend-static | SPA dist server over the fallback seat; index-tap injection | plugin frontend-static |
packages/host/plugin-inventory | Read-only projection of Loader entries for trusted RPC | ctx.pluginInventory |
packages/host/directory-picker | Abstract ctx.directoryPicker capability seam | ctx.directoryPicker |
packages/host/directory-picker-auto | Boot-time resolver that mounts native or browse | plugin directory-picker-auto |
packages/host/directory-picker-native | Native OS chooser backend (native) | registers ctx.directoryPicker |
packages/host/directory-picker-browse | In-app listing/creation backend (browse) | registers ctx.directoryPicker |
packages/boot/app-boot | Shared boot glue: env, config, Loader drive | boot(), profiles |
packages/bundle/web-app | The shipped Web composition (owns dist resolution) | plugin web-app |
What the host owns
The host owns exactly the concerns that need a Node process with network/filesystem access:
- The server — a single
node:httpServer(packages/host/webserver/src/index.ts). It does not serve files and knows no harness concepts; it is a "route-registration carrier." Named routes, HTTP upgrade routes, and a single fallback seat are registered by other plugins near boot. - Static dist serving —
frontend-staticclaims that fallback seat and serves the built@deepseek-ai/dsh-web-frontenddist with SPA semantics. - The plugin inventory —
plugin-inventoryreflects the Cordis Loader's current entries over a Remote call so a trusted in-browser settings panel can render load status. - Directory pickers — a discriminated capability seam (
nativevsbrowse) for choosing a workspace.
The Cordis ship shape
The leaf composition for the shipped shell is packages/bundle/web-app/cordis.patch.yml. Layer 2 of its insert list is where the transport is stood up:
# ── layer 2: transport/service ──────────────────────────────────────────────
- id: webserver
name: '@deepseek-ai/dsh-host-webserver'
inject: [webStartup]
config:
host: !!js ctx.webStartup.host ?? '127.0.0.1'
port: !!js ctx.webStartup.port ?? 3080host and port come from the webStartup provider (the --host/--port/--trusted-host flag family in packages/bundle/web-app/src/startup.ts); it ships with a default port 3080, loopback-only by default.
The webServer service
WebServer extends Service and registers as ctx.webServer. Its schema:
export type WebRouteKind = 'exact' | 'prefix'
export interface WebRoute {
kind: WebRouteKind
path: string // absolute pathname, no trailing slash
handler: (req, res) => void | Promise<void> // may hold the response open (SSE)
}
export interface Config { host: '127.0.0.1' | '0.0.0.0'; port: number }Public surface: register(route), registerUpgrade(route), registerFallback(handler), tapIndex(transform), plus the read-only port/host getters. Duplicate routes, duplicate upgrades, or a second fallback all throw — each is a composition-level contract. Matching is: exact table first, then longest-prefix-wins over the prefix table. The fallback answers anything unclaimed; before its owner registers it returns 404. The upgrade event path negotiates WebSocket-style upgrades.
The real routes at this revision
The webserver package registers nothing itself; composing plugins do. The two concrete carriers in the shipped shell:
| Route | Kind | Owner |
|---|---|---|
API_PATH = '/api' | prefix | packages/client/connection node half via ctx.webServer.register(...) |
/api/events.mux | upgrade | packages/client/connection (client-connection: /api route) |
/api/events.host | upgrade | packages/client/connection |
| the fallback seat | fallback | frontend-static (claimed in web-app apply) |
The /api prefix and the two WebSocket pathnames are defined once in packages/client/connection/src/api-path.ts:
export const API_PATH = '/api'
export const MUX_EVENTS_PATH = `${API_PATH}/events.mux`
export const HOST_EVENTS_PATH = `${API_PATH}/events.host`frontend-static: serving the dist
packages/host/frontend-static/src/index.ts serves the built SPA from the fallback seat. Its Config is a single distIndex (absolute path of dist/index.html); in the shipped composition it is resolved as workspace knowledge inside web-app (resolveDistIndex() in packages/bundle/web-app/src/index.ts), never configured by a deployment.
Semantics (locked at "step1" per the header comment):
- path traversal outside
distRoot→ 403; - any miss →
index.htmlwith 200 (SPA routing); - unknown extension →
application/octet-stream; known MIME table covers.js,.css,.svg,.json,.map,.webmanifest; - non-GET/HEAD → 405;
- every index response runs through the webserver's registered index taps (
ctx.webServer.applyIndexTaps(html)) — this is how the boot manifest andwindow.__DSH_BOOT__injection happens.
web-app mounts it:
ctx.plugin(FrontendStatic, { distIndex: internals.resolveDistIndex() })and additionally provides webRuntime (LAN-trust hosts derived from bind + --trusted-host), registers the app:web-surface and harness:source prompt sections, sets the DSH_WEB_URL shell variable, and prints the URL line after the tree settles.
Plugin inventory
packages/host/plugin-inventory/src/index.ts is a read-only Remote projection of the Live ctx.loader entries. PluginInventoryGateway extends TypertRemoteService and exposes a single @Remote('list') that snapshots non-group entries in Loader order:
interface PluginInventoryEntry {
entryId: PluginEntryId
moduleName: string // exact module specifier imported by the Loader entry
enabled: boolean // effective enablement incl. disabled ancestor groups
fiberPhase: PluginFiberPhase // 'pending'|'loading'|'active'|'failed'|'unloading'|null
}It reads the Loader directly on every call (this.ctx.loader.entries()) — no second cache, because Loader events already maintain the fiber lifecycle. The web surface for it is @deepseek-ai/dsh-client-ui-settings-plugin-inventory (the plugin-inventory tab); the settings "Plugins" panel itself is @deepseek-ai/dsh-client-ui-settings-plugins.
Directory pickers
ctx.directoryPicker is a discriminated capability: a backend exposes one interaction shape, and consumers switch on capability().kind.
interface DirectoryPickerCapabilities {
native: { kind: 'native'; pick(signal: AbortSignal): Promise<string | null> }
browse: { kind: 'browse'; list(path?, signal?): Promise<DirectoryListing>;
createDirectory(path, name): Promise<string> }
}Why two? A native backend opens one OS chooser on the host's display — useless for a remote client. A browse backend serves one-level listing + child creation over Node's stdlib, so it works for remote browsers too.
How auto chooses
packages/host/directory-picker-auto samples the host once at boot (resolveDirectoryPickerBackend in resolve.ts) and mounts the matching pair (backend + client surface) as real Loader entries. Decision order:
- bind host ≠
127.0.0.1→browse(all-interfaces admits remote browsers); SSH_CONNECTION/SSH_TTYset →browse(no local display);darwin/win32→native;linux→nativeonly ifDISPLAY/WAYLAND_DISPLAYpresent and a chooser binary (zenity/kdialog, probed withcanExecute) is on PATH; otherwisebrowse.
Anything ambiguous resolves to browse, "which works everywhere."
Native backend
NativeDirectoryPicker calls pickNativeDirectory (packages/host/directory-picker-native/src/native-picker.ts). Per platform: macOS runs osascript choose folder; Linux runs zenity --file-selection --directory then falls back to kdialog --getexistingdirectory; Windows spawns a child process running a koffi-backed IFileOpenDialog COM conversation (win32-dialog*.ts). Abort (signal) terminates the OS command; user cancel returns null.
Browse backend
BrowseDirectoryPicker (browse/src/index.ts) streams a directory with opendir into a name-sorted bounded window (maxEntries + 1, default 1000 like GitHub's web UI), reports truncated when the level is cut, follows symlinks to directories, marks dot-prefixed entries hidden, and rejects any path that is not fully qualified (never rebased under the host cwd — a deliberate anti-drive-resolution fence on Windows). createDirectory writes one validated single segment.
Both backends keep a stable capability object for the service lifetime, per the seam contract (DirectoryPicker.capability() is abstract, returned once).
app-boot: how host services start
packages/boot/app-boot/src/index.ts is the shared boot glue for the dsh and dsh-acp-demo bins. Its boot() pipeline:
new Context(), setctx.baseUrlto the config directory;process.loadEnvFile('./.env')(opt-in) / layered env vialoadLayeredEnv, rejecting bootstrap-only names (a denylist incl.DSH_*/XDG_*prefixes — a.envmay not set variables that change how the process/runtime/VCS/network boot);- install the fail-loud guard (
installFailLoud) — any lateunhandledRejectionbecomes one labelled stderr diagnostic +exit(1), after giving a terminal-owning surface a chance to restore raw mode; ctx.plugin(Loader); thenmountRootIncludemountscordis:include(the leaf config) pluscordis:groupbuiltin;await ctx.get('loader')?.await(); audit withassertEntriesLoaded/assertEntriesActivated; return the settled rootContext.
resolveConfigPath swaps a cordis.yml basename to cordis.snapshot.yml in replay snapshot mode. The profile launcher (profile.ts) resolves bundles from the installation then the profile dir, and maintains a flat $DSH_HOME/profiles/node_modules symlink fallback so out-of-tree plugins share the installation's single Cordis instance.
Port & bind configuration
| Key / flag | Meaning | Default |
|---|---|---|
webServer.config.host | '127.0.0.1' | '0.0.0.0' | '127.0.0.1' |
webServer.config.port | listen port; 0 = OS-assigned | 3080 |
--host <host> / --port <port> | CLI flags parsed by @deepseek-ai/dsh-web-app/startup | loopback / 3080 |
--trusted-host <authority...> | extra authority the /api browser-trust fence accepts | none |
Note the safety guard in packages/bundle/web-app/src/startup.ts: --host 0.0.0.0 is rejected outright ("expose remote code execution to the network"). The default Harness home is ~/.dsh (override via DSH_HOME) — see packages/util/home-paths.
Further reading
/llm-platform/storage— where host-owned durable state lands (JSON vs SQLite, sessions, projections)./llm-platform/identity-feedback— the anonymous id, attachments, and message feedback services the host composes./architecture/extension-system— the Cordis plugin model every host row relies on./overview/monorepo-anatomy— host vs client package split.- Source:
packages/host/webserver/src/index.ts,packages/host/frontend-static/src/index.ts,packages/host/plugin-inventory/src/index.ts,packages/host/directory-picker-auto/src/resolve.ts,packages/boot/app-boot/src/index.ts. - Composition:
packages/bundle/web-app/cordis.patch.yml,packages/bundle/web-app/src/startup.ts.