dsh-webapi (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
injectswebServerandtools; the routes ride on the
DSH host webserver, plus an independent fixed-port service you can start and stop on demand. - 80+ routes (REST + SSE), documented by a built-in OpenAPI 3.0 spec and a request page, plus a settings page that starts/stops the independent API service and manages API keys.
- Bilingual skill included —
skills/dsh-web-service/SKILL.mdteaches 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):
| Field | Default | Meaning |
|---|---|---|
pathPrefix |
/api/v1 |
Route prefix |
apiKey |
'' |
Non-empty enables auth; requests must carry this key |
standalonePort |
0 |
Initial port on first install; 0 auto-allocates one (31800-31899). Once assigned, the settings page owns it |
standaloneBindIp |
127.0.0.1 |
Initial bind range on first install: 127.0.0.1 (local only) or 0.0.0.0 (LAN reachable); the settings page owns it afterwards |
cors |
true |
Allow cross-origin requests |
defaultCwd |
'' |
Default working directory; empty means process.cwd() |
maxUploadBytes |
2 GiB |
Upload size limit; use the chunked endpoints for bigger files |
adminRemoteAccess |
false |
Whether 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 service (independent fixed port)
The plugin runs its own HTTP service, decoupled from the DSH host webserver: the host port changes
on every launch, while this port stays fixed for clients, scripts and reverse proxies.
- A port is allocated on first install — the first free port from 31800, then it never drifts.
- Start and stop from the settings page —
GET /api/v1/settings/api-keys→ the "API 服务" card.
Port and bind range (127.0.0.1local only /0.0.0.0LAN) apply immediately, no DSH restart. - Live API URL — the address follows the form as you type a port or switch the range; while
running it shows the real address and copies with one click. - Persistent intent — a running service comes back on the same port after a DSH restart; one you
stopped stays stopped.
State lives in /dsh-web-service/service-settings.json (0600); the legacy fixedPort
format is migrated on read.
| Action | Endpoint (admin token) |
|---|---|
| Read state | GET /api/v1/api-keys/service |
| Start / change port / bind range | POST /api/v1/api-keys/service/start { "port": 31800, "bindIp": "127.0.0.1" } |
| Stop | POST /api/v1/api-keys/service/stop |
| Suggest a free port | GET /api/v1/api-keys/service/free-port |
| Legacy compatibility | GET / PUT /api/v1/api-keys/port (fixedPort: 0 stops) |
A failed bind (port in use, …) returns 409 and rolls back to the previous listener, so the current
state is unchanged.
API Key management (settings page)
Open GET /api/v1/settings/api-keys to manage the independent API service and 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
| Entry | Notes |
|---|---|
| "API 服务" in the DSH Web GUI sidebar | The 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-keys |
The standalone page — bookmarkable |
"管理 API Key" button on /docs |
One 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-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):
| Mode | Behaviour |
|---|---|
| Random (default) | Server generates dsk_ + 32 bytes of CSPRNG (256-bit entropy) |
| Use a custom key | Paste 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
| Purpose | Credential | Header |
|---|---|---|
| Call business endpoints (data plane) | API Key | Authorization: Bearer or X-API-Key: |
| Manage keys (admin plane) | Admin Token | Authorization: Bearer or X-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
sealedblob (for the list's "copy" button).sealedis AES-256-GCM under a key HKDF-derived from
the admin token, soapi-keys.jsonalone (backup, screenshot, pasted into an issue) does not
reveal keys — the 0600admin-tokenin 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 gets403 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
apiKeyis 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/statusreports
keysStoreDegraded. The corrupt file is renamed toapi-keys.json.corrupt-for
forensics and the store recovers fromapi-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 keepadminRemoteAccess: falseand 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 thecorsoption. - The admin token is stored in the browser
localStorage; use "clear local admin token" on shared
machines.
The store lives at /dsh-web-service/api-keys.json (mode 0600).
Features
- Workspaces — create, update, delete and list workspaces and their sessions.
- Sessions — create, delete/archive, retitle or switch model, list, and page through message history.
- Models & settings — list models and providers, read and update the global default model, read
settings namespaces, list agent presets. - Streaming —
POST /sessions/:id/prompt-streampushesdelta,reasoning,tool_call,
tool_resultandturn_endover Server-Sent Events;GET /sessions/:id/eventstaps the session's
raw event bus.
Streaming & timeout semantics (important): all three chat entry points (prompt/
prompt-stream/chat/completions) use a stall watchdog instead of a wall-clock turn limit —
continuously producing turns (long text generation, multi-step tool runs) are never cut off no
matter how long they take; only a sustained absence of session events aborts with an explicit
error. Tune viatimeoutMs(sync entry point, stall gap, default 180s) andstallTimeoutMs
(streaming entry point, default 300s). Every SSE channel emits a': ping'comment frame every
15s as a heartbeat so middle proxies/gateways cannot drop idle streams; any internal stream error
surfaces as a visibleerrorevent instead of a silent disconnect. - OpenAI compatibility —
POST /chat/completionsaccepts a standard OpenAI client, streaming or not. - Built-in docs — interactive request page at
GET /api/v1/docs, OpenAPI 3.0 spec at
GET /api/v1/openapi.json. - Interactive sessions — read pending
ask_user_questionbatches and answer them over REST, cancel
a running turn, upload/list/download workspace files (including resumable chunked upload). - API service & key settings page —
GET /api/v1/settings/api-keysstarts/stops the independent
service, edits its port and bind range (with a live URL preview), and manages third-party access
keys: 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
| Module | Method | Path | Notes |
|---|---|---|---|
| System | GET |
/system/status |
System status, live models, port info |
GET |
/ping |
Machine fingerprint (IPs/MACs) to tell whether a 127.0.0.1 (natpass) node is the local machine | |
| Workspaces | GET |
/workspaces |
List workspaces |
POST |
/workspaces |
Create / add a workspace | |
GET |
/workspaces/:id |
Workspace detail | |
PUT |
/workspaces/:id |
Rename a workspace | |
DELETE |
/workspaces/:id |
Remove a workspace binding | |
GET |
/workspaces/:id/sessions |
Sessions in a workspace | |
| Sessions | GET |
/sessions |
List sessions (search + workspace filter) |
POST |
/sessions |
Create a session | |
GET |
/sessions/:id |
Session detail and state | |
PUT |
/sessions/:id |
Update a session (title / model) | |
DELETE |
/sessions/:id |
Delete or archive a session | |
GET |
/sessions/:id/history |
Paged message history | |
GET |
/sessions/:id/stats |
Live 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/todos |
Task 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/skills |
Session-scoped skill catalog (resolves skill roots from the session cwd, supports ?search=; mirrors skills/list for a "/" completion menu) |
|
GET |
/sessions/:id/questions |
Pending ask_user_question batches (host-side answer bridge, ≥0.1.7; connection-layer race bypass + ALS-only session binding — browser/GUI-origin questions always pass through, ≥0.3.7) |
|
POST |
/sessions/:id/answers |
Answer pending questions (answers: [{id, selected, custom?}]); the tool returns as an ordinary tool/result and the session continues |
|
POST |
/sessions/:id/cancel |
Abort the current turn | |
POST |
/sessions/:id/files |
Upload 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/files |
List the session workspace (?path= browses a subdirectory, directories first) |
|
GET |
/sessions/:id/files/download |
Download a workspace file (?path= relative; ?inline=1 for in-browser preview) |
|
| Streaming | POST |
/sessions/:id/prompt-stream |
Send a prompt and receive generation as SSE |
GET |
/sessions/:id/events |
Subscribe to the session event bus as SSE | |
POST |
/sessions/:id/prompt |
Send a prompt and wait for the result synchronously | |
| OpenAI | POST |
/chat/completions |
OpenAI Chat protocol (streaming and non-streaming) |
| Models | GET |
/models |
Available models and the default model |
GET |
/models/default |
Read the global default model | |
PUT |
/models/default |
Update the global default model | |
GET |
/providers |
Registered LLM providers | |
GET |
/presets |
Available agent presets | |
| Settings | GET |
/settings |
Settings namespaces |
PATCH |
/settings/:namespace |
Update one namespace | |
| API Service | GET |
/api-keys/service |
Independent service state (running / port / bind range / reachable URLs) |
POST |
/api-keys/service/start |
Start, or change port / bind range (instant, rolls back on failure) | |
POST |
/api-keys/service/stop |
Stop (keeps the port; does not auto-start after a DSH restart) | |
GET |
/api-keys/service/free-port |
Suggest a free port | |
GET / PUT |
/api-keys/port |
Legacy compatibility (fixedPort: 0 stops) |
|
| API Keys | GET |
/settings/api-keys |
API service & key settings page (HTML, public; data endpoints need the Admin Token) |
GET |
/api-keys |
List API keys (masked; never returns plaintext or hashes) | |
POST |
/api-keys |
Create an API key (plaintext returned; copyable later) | |
PATCH |
/api-keys/:id |
Update name / note / expiry | |
POST |
/api-keys/:id/rotate |
Rotate (old key dies immediately; new key copyable later) | |
POST |
/api-keys/:id/reveal |
Fetch the plaintext for copying (admin token; 409 for legacy keys) | |
POST |
/api-keys/:id/revoke |
Revoke (idempotent, irreversible) | |
DELETE |
/api-keys/:id |
Delete the record (revoked keys only) | |
GET |
/api-keys/auth |
Read auth status | |
PUT |
/api-keys/auth |
Enable / disable auth (disabling requires confirm: "disable-auth") |
|
| Docs | GET |
/docs |
Interactive API request page |
GET |
/docs/reference |
Full API reference & online debugging (Swagger UI, try-it-out) | |
GET |
/openapi.json |
OpenAPI 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