Skip to content

The host-side /api surface used to be one package, packages/host/apiproxy (ctx.apiProxy) — the four-quadrant RPC wire with an SSE server-request channel and a /api/respond answer route. That package was deleted (commit 4f00a8b82a); the shared route is now owned by the packages/api/* family. This page maps the replacement: what owns the /api route today, how unary RPC and live streams travel, and how the browser authenticates against it.

PackageRole
@deepseek-ai/dsh-api-gatewayTwo-sided Typert RPC endpoint: Host ctx.typertGateway, Client ctx.remote; owns the remote-stream WebSocket mux
@deepseek-ai/dsh-api-remotesThe application-level BFF: which Host Remote contributions this Client assembly mounts
@deepseek-ai/dsh-api-session-controllerSession commands, history streams, and live control state
@deepseek-ai/dsh-api-settings-controllerConfiguration read/write (settings + credentials namespaces)
@deepseek-ai/dsh-api-workspace-controllerWorkspace mutations and the reconnect-safe state feed

What owns the /api route ​

The physical route is still one prefix registered at API_PATH = '/api' (packages/client/connection/src/api-path.ts). Its handler is the Connection node half's HTTP bridge (packages/client/connection/src/http-bridge.ts) dispatching into a shared fetch handler:

  1. Browser-trust fence + browser-session auth first — every request passes the Host/Origin trust check (isTrustedApiRequest, a DNS-rebinding defense) and persistent authentication before any dispatch; a rejected request gets 401 / 403 without touching a controller (see browser-session authentication below).
  2. Exact fetch routes win. The handler selects one registered fetch route by pathname; anything without an exact route answers 404 (new Response('not found', { status: 404 })) — there is no "fall through to the API proxy" anymore. Generated Remote namespaces (from Typert codecs) and a few explicit routes (the Remote event stream, index auth) are what get registered.
  3. The body cap is 300 MiB. DEFAULT_MAX_REQUEST_BODY_BYTES = 300 * 1024 * 1024 in http-bridge.ts — sized for the default aggregate image limit after base64 expansion plus envelope headroom. Over-length requests are refused with 413 before buffering completes.
  4. Media type is enforced. A unary RPC payload whose content-type is not application/json is refused with 415 — so a cross-site "simple" request (which browsers send without a CORS preflight) can never execute a side-effectful method blind.

Transport: unary RPC + one stream WebSocket ​

The old carrier had per-stream WebSockets (/api/events.mux, /api/events.host) plus an SSE server-request channel and a /api/respond POST. All of those are gone. Today there are exactly two physical channels:

  • Unary RPC: fetch POST to /api/<endpoint> with the typed envelope ClientRequest ({ type: 'client-request', rpcId, method, payload }) answered by ServerResponse ({ type: 'server-response', rpcId, result }) — see Frontend: Client runtime for the schema pair.
  • Live Remote streams: one shared-mux WebSocket at /api/remote.mux (REMOTE_STREAM_MUX_PATH = '/api/remote.mux', packages/api/gateway/src/stream-protocol.ts). The gateway owns the upgrade route; a single shared socket multiplexes independently cancellable Remote streams, and the server sends WebSocket Ping heartbeat frames (DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL_MS = 2_000) with pong tracking to detect dead peers. Browser side: RemoteStreamMuxClient (packages/api/gateway/src/client/stream-client.ts), which opens ws(s)://<origin>/api/remote.mux and fans frames out to per-stream readers.

Stream endpoints are declared with @Remote({ mode: 'stream' }) (Typert protocol; any other options shape is rejected): the generated Client face gives each one a stream carrier object — RemoteStream<Item> (plain incremental stream), RemoteSnapshotStream<Snapshot, Delta> (complete baseline followed by replacement frames), and RemoteJournalStream (page/entry/cursor journal reads) — all sharing the one physical socket.

txt
Browser                                   Host
  ├─ fetch POST /api/session.prompt ──────►  /api prefix route (trust + auth, 300 MiB cap)
  ├─ fetch POST /api/settings.mutate ─────►  exact fetch route → Typert gateway dispatch
  └─ WS /api/remote.mux ◄──────────────────  gateway stream mux (Ping keepalive)
        ├─ session/control   (baseline + replacement frames)
        ├─ session/follow    (history journal)
        └─ workspace/follow  (baseline + increments)

session-controller: Sessions ​

packages/api/session-controller (namespace ctx.remote.session) owns the Session-facing business API:

  • Commands (src/commands.ts): create (idempotent adopt), rename, fork, prompt (admit one prompt after resuming), attachment (read a log-reachable image), updateQueue, cancel, selectModel, plus openWorkspacePath / canOpenWorkspacePath.
  • History & listing: list / search (cold, non-resuming), and two streams — page (cold-safe message-aligned pages) and follow (@Remote({ mode: 'stream' }): a complete opening snapshot followed by gap-free event frames).
  • Live control state: control (@Remote({ mode: 'stream' })) streams a complete live-control baseline then replacement frames — the state the composer, queue, and turn indicators render.
  • Also mounted from here: modelCatalog (provider-grouped routable models + deployment default), a skill catalog (skill-catalog.ts), file-reference resolution (file-references.ts), and Agent-inspection (agent.ts).

settings-controller: configuration + credentials ​

packages/api/settings-controller owns the configuration plane as two namespaces:

  • settings (src/index.ts): describe() (every namespace redacted under redactSecrets: true — returns { writable, hasDocument, namespaces }), update(ns, patch, expectedRevision), replace(ns, section, expectedRevision), mutate(ns, pathOps, expectedRevision), plus openSettingsDocument / openAgentPresetDirectory / canOpenAgentPresetDirectory. Write refusals classify as settings/conflict (stale revision) or settings/rejected.
  • credentials (src/credentials.ts): describe(refs) (batched, ≤ 64), set(ref, value), unset(ref) — secret values cross one direction only, so no read path can echo one.

This is the Remote transport behind the settings model layer — the browser side of the seam is covered in Settings Schema & Forms.

workspace-controller: Workspaces ​

packages/api/workspace-controller (namespace ctx.remote.workspace) owns Workspace mutations: create, rename, delete, insertBefore, insertSessionBefore, archiveSession, and the follow stream (@Remote({ mode: 'stream' }): complete baseline then ordered increments). It also mounts the directory-picking namespace (directory-picker.ts) — the browse/native picker seam exposed over Remote.

gateway + remotes: dispatch and the assembly ​

Two packages/api neighbors complete the layer:

  • dsh-api-gateway is the Typert dispatch engine: Host ctx.typertGateway.invoke() resolves the descriptor and Cordis service per call, validates exact named arguments, resolves Agent/Session lookups, injects cancellation, and validates results; Client side ClientRemote (ctx.remote) mounts contributions via $mount() and subscribes to forwarded events via $on(). Stream-mode endpoints are served by the gateway's RemoteStreamMuxServer over the mux WebSocket. See API Gateway.
  • dsh-api-remotes is the application's BFF assembly: Host entry owns the Agent/Session identity policy (createApiRemoteAgentResolver); Client entry selects and mounts the generated Remote contributions. At this revision the Client assembly mounts 12 namespaces — agentPresets, commands, settingsController, goals, llm, dynamic, pluginInventory, messageFeedback, sessionReferences, subagents, session, workspace — in one ctx.remote.$mount() loop (packages/api/remotes/src/client/index.ts:146-150).

Browser-session authentication ​

HEAD adds full browser-session authentication on the /api plane (packages/client/connection/src/browser-auth.ts), replacing the old "no credentials" stance with a signed-token exchange:

  • On process start the host mints a per-process HMAC launch token (32 random bytes) and prints the authenticated URL — authenticatedUrl(baseUrl) appends ?token=<launchToken> to the root path. Supervisors and the dsh web readiness line use that URL.
  • A browser opening that URL exchanges the token once: GET / with exactly one valid ?token= mints a dsh-auth-<authority> cookie (HMAC-SHA256-signed payload { version, authority, issuedAt, expiresAt }, HttpOnly; SameSite=Strict) and answers 303 to clean /. The cookie is authority-bound and expires after cookieMaxAgeDays (default 30).
  • Every other request without a valid cookie gets the same minimal 401 body ("reopen the URL printed by dsh web"). Valid-cookie requests pass isAuthenticated() and reach the /api prefix.
  • The trust fence still applies on top: isTrustedApiRequest/assertTrustedAuthority reject any Host that is neither loopback nor a configured trustedHosts authority — the old PRIVILEGED_METHODS loopback pin list is gone (see Frontend: Client runtime).

Where model traffic enters ​

Model traffic no longer rides an "API proxy relay". The client reaches models through the llm Remote (llm.discoverModels, llm.providers) for pickers, and an actual prompt enters the agent through session.prompt — the session-controller prompt path resumes the Session and admits the prompt, after which the agent loop calls ctx.llm.stream host-side. There is no client-to-provider request relay on /api.

Key source files ​

Repo-relative pathWhat it provides
packages/api/gateway/src/stream-protocol.tsREMOTE_STREAM_MUX_PATH, wire frames, heartbeat contract
packages/api/gateway/src/stream-server.tsRemoteStreamMuxServer, Ping keepalive
packages/api/gateway/src/client/ClientRemote, RemoteStream/RemoteSnapshotStream/RemoteJournalStream
packages/api/remotes/src/client/index.tsThe 12-name Remote assembly (apply, $mount loop)
packages/api/session-controller/src/index.tsSession commands, follow, control
packages/api/settings-controller/src/index.ts + src/credentials.tssettings/credentials namespaces
packages/api/workspace-controller/src/index.tsWorkspace mutations + follow
packages/client/connection/src/http-bridge.tsDEFAULT_MAX_REQUEST_BODY_BYTES, the /api bridge
packages/client/connection/src/browser-auth.tsauthenticatedUrl, cookie minting, authorizeIndex, 401

Further reading ​