← Back to home@toddpan

dsh-webapi

DSH plugin — drive DeepSeek Harness over HTTP: 47 REST + SSE routes (workspaces, sessions, streaming, stats, todos, models, settings, skills, files) + OpenAI-compatible /chat/completions. / DSH 插件:把 DSH 开放成 47 条 REST + SSE 接口与 OpenAI 兼容端点,供三方客户端与 Agent 集成。

Stars
1
Language
TypeScript
Created
Sep 3, 2026
Updated
Oct 6, 2026
GitHub repo

Introduction

dsh-webapi (@dsh-external/dsh-web-service)

English | 中文

A DSH plugin that exposes DeepSeek Harness over HTTP: the workspaces, sessions, models, settings, skills and files already inside DSH become plain REST endpoints and SSE streams, plus an OpenAI-compatible /chat/completions so an existing OpenAI client can talk to DSH unchanged.

Typical uses: a self-hosted console for DSH, a desktop or mobile client, CI / automation that drives an agent, a non-DSH agent that calls DSH as a tool, or a gateway that puts DSH behind your own API.

  • No new dependencies — the plugin only injects webServer and tools; the routes ride on the DSH host webserver (or an optional standalone port).
  • 60 routes (REST + SSE), documented by a built-in OpenAPI 3.0 spec and a request page, plus an API Key settings page.
  • Bilingual skill included — skills/dsh-web-service/SKILL.md teaches a DSH agent how to drive the API (streaming, cancelling, answering pending questions, file uploads).

Install

Requires DSH 0.1.0–0.2.x (peer ranges in package.json; verified on 0.1.7-rc.2, and type-checked plus assembly-verified on 0.2.0-rc.2).

Option 1: prebuilt tarball (recommended — no build step, no allowBuilds approval)

dsh plugin --profile web add \
  https://github.com/toddpan/dsh-webapi/releases/latest/download/dsh-web-service.tgz

dsh plugin records the package in the profile package.json (dependency + dsh.profile.bundles); a profile with HMR assembles it immediately, otherwise restart DSH. Verify:

curl -s http://127.0.0.1:3080/api/v1/system/status   # {"ok":true,...}
curl -sI http://127.0.0.1:3080/api/v1/docs           # HTTP/1.1 200 OK

Upgrading. The asset name carries no version, so latest/download/dsh-web-service.tgz always points at the newest build for a fresh install — but package managers may reuse a cached resolution of that URL. To move an existing install to a known build, pin the versioned asset: dsh plugin --profile web add https://github.com/toddpan/dsh-webapi/releases/download/v0.1.11/dsh-web-service-0.1.11.tgz

Option 2: install straight from the git repo (the "GitHub repository" install in the plugin manager)

dsh plugin --profile web add github:toddpan/dsh-webapi

lib/ is committed, so a git install is directly runnable. It is kept in sync with src/; after changing src/, rebuild and commit lib/ in the same change.

Option 3: build from source (needs a DSH source checkout)

git clone https://github.com/toddpan/dsh-webapi && cd dsh-webapi
DSH_CHECKOUT=/path/to/deepseek-harness bash scripts/build.sh     # src/ → lib/
dsh plugin --profile web add "$PWD"

scripts/build.sh symlinks cordis / schemastery / @deepseek-ai/* peers out of the checkout, so no npm install is needed.

Configuration

All fields are optional; they live in the plugin row's config in your profile patch (defaults in src/index.ts):

FieldDefaultMeaning
pathPrefix/api/v1Route prefix
apiKey''Non-empty enables auth; requests must carry this key
standalonePort0>0 also listens on its own port; 0 means host webserver only
corstrueAllow cross-origin requests
defaultCwd''Default working directory; empty means process.cwd()
maxUploadBytes2 GiBUpload size limit; use the chunked endpoints for bigger files
adminRemoteAccessfalseWhether non-loopback clients may reach the API Key admin endpoints; key management is local-only by default

apiKey is only a legacy single key, kept for backward compatibility. Manage keys from the settings page instead — see API Key management.

API Key management (settings page)

Open GET /api/v1/settings/api-keys to manage the keys third parties use to call the API: view (masked), create, rotate, revoke, and turn authentication on or off. The /docs page also links to it from its header.

Three entry points

EntryNotes
"API Key 管理" in the DSH Web GUI sidebarThe plugin ships a browser half (client half); after a DSH restart the panel appears in the sidebar and embeds this page
GET /api/v1/settings/api-keysThe standalone page — bookmarkable
"管理 API Key" button on /docsOne click from the API docs

The page sits behind an admin token gate: key management and normal API calls use two separate credentials. On first open it asks for the admin token and shows the exact command to read it (run on the machine that hosts DSH): cat <DSH home>/dsh-web-service/admin-token. The token is then kept in the browser's localStorage. Until the gate passes, management buttons such as "新建 API Key" stay hidden — no "click, then hit 401" dead ends.

Theming and offline availability

All three pages (/docs, /docs/reference, /settings/api-keys) share the HARNESS design tokens (--dsw-alias-*, values mirrored from packages/client/ui-theme/src/styles/design-platform.css), so colours, radii and type match the DSH Web GUI, in both light and dark:

  • Embedded in the GUI sidebar (same-origin iframe) the page reads the parent document's token values and follows its theme switches live — user-customised themes included;
  • Opened standalone it follows prefers-color-scheme, and honours ?theme=light / ?theme=dark.

Swagger UI is self-hosted from vendor/swagger-ui/ and served at /api/v1/docs/assets/*, so /docs/reference needs no public CDN and works on intranets and offline.

Inside the GUI sidebar, in-page buttons such as "Swagger / OpenAPI JSON / interactive docs" navigate within the panel (the target page offers a "← back" button). DSH Desktop's main window denies all new windows (webContents.setWindowOpenHandler(() => ({ action: 'deny' }))), so target="_blank" fails silently inside the app; external links still try a new window and fall back to a copy-the-link hint when refused.

Creating a key offers two sources for the key value (the plaintext field on POST /api-keys):

ModeBehaviour
Random (default)Server generates dsk_ + 32 bytes of CSPRNG (256-bit entropy)
Use a custom keyPaste an existing key (16–256 visible ASCII chars) to slot into an existing client or migrate

The strength of a custom key is your responsibility, so it never shows even a prefix — otherwise my-secret-api-key-xxx would leak most of its value in the first 12 characters. The server still stores only its hash, and the list marks such records as custom. A value that already has an active record is rejected (400); revoked/expired values may be registered again.

Two separate credentials

PurposeCredentialHeader
Call business endpoints (data plane)API KeyAuthorization: Bearer <key> or X-API-Key: <key>
Manage keys (admin plane)Admin TokenAuthorization: Bearer <admin-token> or X-Admin-Token: <admin-token>

So a third party holding an API key cannot mint new keys for itself. The Admin Token is generated on first startup and stored under the DSH config root in dsh-web-service/admin-token (mode 0600):

cat "${DSH_HOME:-$HOME/.dsh}/dsh-web-service/admin-token"

Security contract

  • Two things are persisted per key: sha256(plaintext) (for authentication) and a reversible sealed blob (for the list's "copy" button). sealed is AES-256-GCM under a key HKDF-derived from the admin token, so api-keys.json alone (backup, screenshot, pasted into an issue) does not reveal keys — the 0600 admin-token in the same directory is also required.
  • Every key in the list has a "copy" button: the plaintext is fetched on demand over an admin-token-authenticated endpoint and written straight to the clipboard — never into the DOM, localStorage, or logs.
  • Keys created before sealed storage shipped only have a hash and cannot be recovered; "copy" guides you to "rotate" instead. Deleting/recreating the admin token invalidates old sealed blobs, which likewise degrades to "not copyable" rather than silently returning a wrong value.
  • Admin endpoints accept loopback clients only by default. Remote key management requires adminRemoteAccess: true. Browser requests are same-origin checked (CSRF / DNS-rebinding); cross-origin gets 403 FORBIDDEN_ORIGIN.
  • Revoking is immediate and irreversible; rotating invalidates the old key at once. The safer path is: create a replacement key, roll it out, then revoke the old one.
  • When apiKey is set in the plugin config, auth is forced on and cannot be switched off from the page. Auth cannot be switched on when no valid key exists, which would lock you out.
  • A corrupt key store fails closed: auth is never relaxed and /system/status reports keysStoreDegraded. The corrupt file is renamed to api-keys.json.corrupt-<timestamp> for forensics and the store recovers from api-keys.json.bak (the last known good state, refreshed after every successful write), so existing key records are never silently dropped.
  • The loopback guard looks at the TCP peer address. If DSH sits behind a reverse proxy or tunnel the plugin sees the proxy as the peer, so the guard is effectively bypassed — close it off at the network layer, or keep adminRemoteAccess: false and manage keys locally only.
  • Data-plane auth is a Bearer/API-Key check with no same-origin rule: it is built for cross-origin third-party clients, so a browser page sending a valid key cross-origin is by design. The cross-origin surface is governed by the cors option.
  • The admin token is stored in the browser localStorage; use "clear local admin token" on shared machines.

The store lives at <DSH_HOME>/dsh-web-service/api-keys.json (mode 0600).

Features

  1. Workspaces — create, update, delete and list workspaces and their sessions.
  2. Sessions — create, delete/archive, retitle or switch model, list, and page through message history.
  3. Models & settings — list models and providers, read and update the global default model, read settings namespaces, list agent presets.
  4. Streaming — POST /sessions/:id/prompt-stream pushes delta, reasoning, tool_call, tool_result and turn_end over Server-Sent Events; GET /sessions/:id/events taps the session's raw event bus.
  5. OpenAI compatibility — POST /chat/completions accepts a standard OpenAI client, streaming or not.
  6. Built-in docs — interactive request page at GET /api/v1/docs, OpenAPI 3.0 spec at GET /api/v1/openapi.json.
  7. Interactive sessions — read pending ask_user_question batches and answer them over REST, cancel a running turn, upload/list/download workspace files (including resumable chunked upload).
  8. API Key settings page — GET /api/v1/settings/api-keys manages third-party access keys in one place: multiple named keys, masked display, create / rotate / revoke, and an auth on/off switch. Hashes plus an admin-token-encrypted sealed copy are persisted.

API routes

Default prefix: /api/v1

ModuleMethodPathNotes
SystemGET/system/statusSystem status, live models, port info
WorkspacesGET/workspacesList workspaces
POST/workspacesCreate / add a workspace
GET/workspaces/:idWorkspace detail
PUT/workspaces/:idRename a workspace
DELETE/workspaces/:idRemove a workspace binding
GET/workspaces/:id/sessionsSessions in a workspace
SessionsGET/sessionsList sessions (search + workspace filter)
POST/sessionsCreate a session
GET/sessions/:idSession detail and state
PUT/sessions/:idUpdate a session (title / model)
DELETE/sessions/:idDelete or archive a session
GET/sessions/:id/historyPaged message history
GET/sessions/:id/statsLive stats: turns/steps, LLM and tool timings, mean first token, decode throughput, cache hits, token ledger (matches the harness session-stats projection)
GET/sessions/:id/todosTask list + elapsed time: todos[{content,status}] (todo_write full-table projection, cleared on turn/start), counts{completed,inProgress,pending}, running/elapsedMs (wall clock of the current or last turn), turnStartedAt/turnEndedAt/updatedAt — for a third-party console's "tasks" panel (≥0.1.8)
GET/sessions/:id/skillsSession-scoped skill catalog (resolves skill roots from the session cwd, supports ?search=; mirrors skills/list for a "/" completion menu)
GET/sessions/:id/questionsPending ask_user_question batches (host-side answer bridge, ≥0.1.7; includes the connection-layer race bypass and ALS session binding)
POST/sessions/:id/answersAnswer pending questions (answers: [{id, selected, custom?}]); the tool returns as an ordinary tool/result and the session continues
POST/sessions/:id/cancelAbort the current turn
POST/sessions/:id/filesUpload into the session workspace (multipart, or raw + ?filename=); duplicate names get -1 suffixes, and the agent can read them with its file tools
GET/sessions/:id/filesList the session workspace (?path= browses a subdirectory, directories first)
GET/sessions/:id/files/downloadDownload a workspace file (?path= relative; ?inline=1 for in-browser preview)
StreamingPOST/sessions/:id/prompt-streamSend a prompt and receive generation as SSE
GET/sessions/:id/eventsSubscribe to the session event bus as SSE
POST/sessions/:id/promptSend a prompt and wait for the result synchronously
OpenAIPOST/chat/completionsOpenAI Chat protocol (streaming and non-streaming)
ModelsGET/modelsAvailable models and the default model
GET/models/defaultRead the global default model
PUT/models/defaultUpdate the global default model
GET/providersRegistered LLM providers
GET/presetsAvailable agent presets
SettingsGET/settingsSettings namespaces
PATCH/settings/:namespaceUpdate one namespace
API KeysGET/settings/api-keysAPI Key settings page (HTML, public; data endpoints need the Admin Token)
GET/api-keysList API keys (masked; never returns plaintext or hashes)
POST/api-keysCreate an API key (plaintext returned; copyable later)
PATCH/api-keys/:idUpdate name / note / expiry
POST/api-keys/:id/rotateRotate (old key dies immediately; new key copyable later)
POST/api-keys/:id/revealFetch the plaintext for copying (admin token; 409 for legacy keys)
POST/api-keys/:id/revokeRevoke (idempotent, irreversible)
DELETE/api-keys/:idDelete the record (revoked keys only)
GET/api-keys/authRead auth status
PUT/api-keys/authEnable / disable auth (disabling requires confirm: "disable-auth")
DocsGET/docsInteractive API request page
GET/docs/referenceFull API reference & online debugging (Swagger UI, try-it-out)
GET/openapi.jsonOpenAPI 3.0 document

Development

# Build the plugin
bash scripts/build.sh

# Hot-inject at runtime (needs dsh-super-injector)
dev_inject_plugin {"dir": "/path/to/dsh-web-service"}

# Hot reload
dev_reload_package {"packageName": "dsh-web-service"}

Skill: teach an agent to drive this API

The repository ships a DSH-native skill (skills/dsh-web-service/SKILL.md). Once installed, a DSH agent discovers it automatically and knows how to use every endpoint (triggers: driving DSH over HTTP, third-party integration, OpenAI-compatible calls; also available in-session as /dsh-web-service).

Install online (recommended)

curl -fsSL https://raw.githubusercontent.com/toddpan/dsh-webapi/main/scripts/install-skill.sh | bash

It installs into the user-level skill directory ~/.dsh/skills/dsh-web-service/, where DSH's skill-filesystem provider discovers it live (no restart) — usable from the next session.

Options

# Install into a specific directory (project-level .dsh/skills or .agents/skills)
curl -fsSL .../install-skill.sh | bash -s -- --dir /path/to/project/.dsh/skills

# Pick a branch / repository
curl -fsSL .../install-skill.sh | bash -s -- --branch dev
curl -fsSL .../install-skill.sh | bash -s -- --repo other/dsh-webapi

# Uninstall
curl -fsSL .../install-skill.sh | bash -s -- uninstall
# or locally: bash install-skill.sh uninstall

What the script does: download SKILL.md → validate the frontmatter → install into the target directory → probe ports 3080/3000 for a running DSH Web Service and report. Idempotent.

Local install (inside the repo)

bash scripts/install-skill.sh

License

BSD-3-Clause