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.
| Package | Role |
|---|---|
@deepseek-ai/dsh-api-gateway | Two-sided Typert RPC endpoint: Host ctx.typertGateway, Client ctx.remote; owns the remote-stream WebSocket mux |
@deepseek-ai/dsh-api-remotes | The application-level BFF: which Host Remote contributions this Client assembly mounts |
@deepseek-ai/dsh-api-session-controller | Session commands, history streams, and live control state |
@deepseek-ai/dsh-api-settings-controller | Configuration read/write (settings + credentials namespaces) |
@deepseek-ai/dsh-api-workspace-controller | Workspace 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:
- 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 gets401/403without touching a controller (see browser-session authentication below). - 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. - The body cap is 300 MiB.
DEFAULT_MAX_REQUEST_BODY_BYTES = 300 * 1024 * 1024inhttp-bridge.ts— sized for the default aggregate image limit after base64 expansion plus envelope headroom. Over-length requests are refused with413before buffering completes. - Media type is enforced. A unary RPC payload whose
content-typeis notapplication/jsonis refused with415— 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:
fetchPOST to/api/<endpoint>with the typed envelopeClientRequest({ type: 'client-request', rpcId, method, payload }) answered byServerResponse({ 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 opensws(s)://<origin>/api/remote.muxand 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.
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, plusopenWorkspacePath/canOpenWorkspacePath. - History & listing:
list/search(cold, non-resuming), and two streams —page(cold-safe message-aligned pages) andfollow(@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 underredactSecrets: true— returns{ writable, hasDocument, namespaces }),update(ns, patch, expectedRevision),replace(ns, section, expectedRevision),mutate(ns, pathOps, expectedRevision), plusopenSettingsDocument/openAgentPresetDirectory/canOpenAgentPresetDirectory. Write refusals classify assettings/conflict(stale revision) orsettings/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-gatewayis the Typert dispatch engine: Hostctx.typertGateway.invoke()resolves the descriptor and Cordis service per call, validates exact named arguments, resolves Agent/Session lookups, injects cancellation, and validates results; Client sideClientRemote(ctx.remote) mounts contributions via$mount()and subscribes to forwarded events via$on(). Stream-mode endpoints are served by the gateway'sRemoteStreamMuxServerover the mux WebSocket. See API Gateway.dsh-api-remotesis 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 onectx.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 thedsh webreadiness line use that URL. - A browser opening that URL exchanges the token once:
GET /with exactly one valid?token=mints adsh-auth-<authority>cookie (HMAC-SHA256-signed payload{ version, authority, issuedAt, expiresAt },HttpOnly; SameSite=Strict) and answers303to clean/. The cookie is authority-bound and expires aftercookieMaxAgeDays(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/apiprefix. - The trust fence still applies on top:
isTrustedApiRequest/assertTrustedAuthorityreject anyHostthat is neither loopback nor a configuredtrustedHostsauthority — the oldPRIVILEGED_METHODSloopback 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 path | What it provides |
|---|---|
packages/api/gateway/src/stream-protocol.ts | REMOTE_STREAM_MUX_PATH, wire frames, heartbeat contract |
packages/api/gateway/src/stream-server.ts | RemoteStreamMuxServer, Ping keepalive |
packages/api/gateway/src/client/ | ClientRemote, RemoteStream/RemoteSnapshotStream/RemoteJournalStream |
packages/api/remotes/src/client/index.ts | The 12-name Remote assembly (apply, $mount loop) |
packages/api/session-controller/src/index.ts | Session commands, follow, control |
packages/api/settings-controller/src/index.ts + src/credentials.ts | settings/credentials namespaces |
packages/api/workspace-controller/src/index.ts | Workspace mutations + follow |
packages/client/connection/src/http-bridge.ts | DEFAULT_MAX_REQUEST_BODY_BYTES, the /api bridge |
packages/client/connection/src/browser-auth.ts | authenticatedUrl, cookie minting, authorizeIndex, 401 |
Further reading
- API Gateway — the Typert gateway and
ClientRemotein depth - Host Platform — the webserver that mounts
/apiand the mux route - Frontend: Client Runtime — the browser envelopes, trust fence, and lifecycle
- Frontend: Settings Schema & Forms — the settings-controller consumers
- Repo:
packages/api/*/README.md,packages/client/connection/src/browser-auth.ts