Skip to content

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.

PackageRolectx key / seam
packages/host/webservernode:http server; HTTP + upgrade + fallback route registriesctx.webServer
packages/host/frontend-staticSPA dist server over the fallback seat; index-tap injectionplugin frontend-static
packages/host/plugin-inventoryRead-only projection of Loader entries for trusted RPCctx.pluginInventory
packages/host/directory-pickerAbstract ctx.directoryPicker capability seamctx.directoryPicker
packages/host/directory-picker-autoBoot-time resolver that mounts native or browseplugin directory-picker-auto
packages/host/directory-picker-nativeNative OS chooser backend (native)registers ctx.directoryPicker
packages/host/directory-picker-browseIn-app listing/creation backend (browse)registers ctx.directoryPicker
packages/boot/app-bootShared boot glue: env, config, Loader driveboot(), profiles
packages/bundle/web-appThe 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:http Server (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 servingfrontend-static claims that fallback seat and serves the built @deepseek-ai/dsh-web-frontend dist with SPA semantics.
  • The plugin inventoryplugin-inventory reflects 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 (native vs browse) 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:

yaml
# ── 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 ?? 3080

host 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:

ts
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:

RouteKindOwner
API_PATH = '/api'prefixpackages/client/connection node half via ctx.webServer.register(...)
/api/events.muxupgradepackages/client/connection (client-connection: /api route)
/api/events.hostupgradepackages/client/connection
the fallback seatfallbackfrontend-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:

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 distRoot403;
  • any miss → index.html with 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 and window.__DSH_BOOT__ injection happens.

web-app mounts it:

ts
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:

ts
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.

ts
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:

  1. bind host ≠ 127.0.0.1browse (all-interfaces admits remote browsers);
  2. SSH_CONNECTION/SSH_TTY set → browse (no local display);
  3. darwin/win32native;
  4. linuxnative only if DISPLAY/WAYLAND_DISPLAY present and a chooser binary (zenity/kdialog, probed with canExecute) is on PATH; otherwise browse.

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:

  1. new Context(), set ctx.baseUrl to the config directory;
  2. process.loadEnvFile('./.env') (opt-in) / layered env via loadLayeredEnv, rejecting bootstrap-only names (a denylist incl. DSH_*/XDG_* prefixes — a .env may not set variables that change how the process/runtime/VCS/network boot);
  3. install the fail-loud guard (installFailLoud) — any late unhandledRejection becomes one labelled stderr diagnostic + exit(1), after giving a terminal-owning surface a chance to restore raw mode;
  4. ctx.plugin(Loader); then mountRootInclude mounts cordis:include (the leaf config) plus cordis:group builtin;
  5. await ctx.get('loader')?.await(); audit with assertEntriesLoaded / assertEntriesActivated; return the settled root Context.

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 / flagMeaningDefault
webServer.config.host'127.0.0.1' | '0.0.0.0''127.0.0.1'
webServer.config.portlisten port; 0 = OS-assigned3080
--host <host> / --port <port>CLI flags parsed by @deepseek-ai/dsh-web-app/startuploopback / 3080
--trusted-host <authority...>extra authority the /api browser-trust fence acceptsnone

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.