Files
deepseek-harness/docs/subsystems/http-server.md
Tianyi Cui 44b035c711 docs: qualify plan-mode timing and workspace no-write claims
ds-review-bot round 3:
- set() after a turn's final step lands in a later turn (selection
  never forces continuation) — the same-turn phrasing overstated the
  guarantee; set() JSDoc + subsystem page (both languages).
- Workspace attach/insert/detach no-op claims now carry the
  filtered-candidate-prune caveat: mutate() durably prunes stale
  candidates even when the requested change is a no-op, a state the
  sessionIds contract explicitly permits; Workspace fence re-synced on
  both page sides; catalogs regenerated.
2026-08-09 01:28:29 +08:00

3.8 KiB

HTTP Server

English | 中文

dsh-host-webserver is the web-shape HTTP carrier for the GUI host: a single node:http plugin providing ctx.httpServer, a named-route registry, index.html transform taps, and a single claimable fallback seat. It is not part of the agent-loop spine and not a capability seam — it knows no harness concepts, and every feature surface (the /api bridge, plugin bundles, the HMR event stream) is a route some other plugin registers (layering note). Web (browser) shape only: Electron loads dist over file:// and carries fetch over an IPC bridge, not this server.

Source: packages/host/webserver/src/index.ts

Routes

/** Route match kind: 'exact' matches the pathname verbatim; 'prefix' p matches p and p/<anything>. */
type WebRouteKind = 'exact' | 'prefix'
/** One named route registration. */
interface WebRoute {
  kind: WebRouteKind
  /** Absolute pathname, no trailing slash. */
  path: string
  /** Owns the full response lifecycle (may hold the response open, e.g. SSE). */
  handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
}

Match order is fixed: exact table first, then longest matching prefix, then the registered fallback. Registration order carries no request-facing semantics — named routes are composed to be disjoint, and the fallback seat answers anything no named route claims; one owner only, a second registration throws. The shipped Web composition claims the seat with dsh-frontend-static, the SPA dist server with locked semantics: non-GET/HEAD is 405, traversal outside the dist root is 403, any miss falls back to index.html with HTTP 200 (SPA routing), and unknown extensions ship as octet-stream.

Config

/** Gateway config: the listen address. */
interface Config {
  /** Listen host; the two supported values are loopback and all-interfaces. */
  host: '127.0.0.1' | '0.0.0.0'
  /** Listen port; zero requests an OS-assigned port. */
  port: number
}

host accepts only 127.0.0.1 (default posture) and 0.0.0.0 (deliberate network exposure); there is no TLS, auth, or origin policy, so a non-loopback bind exposes the server to that network. The dist location is an assembly fact of the frontend plugin that claims the seat.

The service

HttpServerService (ctx.httpServer) listens immediately on activation; a listen failure (EADDRINUSE…) throws out of init — a FAILED fiber the boot's fail-loud sweep reports. register(route) adds one named route and returns its disposer; a duplicate (kind, path) throws, because route patterns are a composition-level contract and a collision is a misconfiguration. tapIndex(transform) adds a pure html-to-html transform applied to every index response — / and each SPA fallback — in registration order; dsh-client-modules uses it to inject the boot manifest. port reads the listening port, the OS-assigned value when config.port is 0.

A request whose handling throws (a malformed %-escape hitting decodeURIComponent, a client dropping mid-body) is logged as a warning and answered 400 — or the socket destroyed when headers are already out — never a process exit. Disposal pairs close() with closeAllConnections() because a handler may hold its response open (SSE) and such connections never end on their own; without the force-close, teardown would hang. The package never prints: the URL line belongs to the shell. Per-package operational detail, including the dev-mode bundle watch pipeline, stays in the README.