litestartup-com / dsh-api-gateway

Listed

DeepSeek Harness's API Gateway plugin: Any third-party client can interact with your DSH Agent.

mainOther View source

Installation

npx -y @deepseek-ai/dsh plugin --profile web add github:litestartup-com/dsh-api-gateway

This installation command is an unverified starting point generated from the GitHub repository address.

README

Maintainer-authored documentation snapshot.

View on GitHub ↗
Commit 855aa8aSynced Aug 18, 2026

dsh-api-gateway

English | 中文

A plugin for DeepSeek Harness that turns a running Harness into an HTTP API: any third-party client — curl, Python, a browser, an IM bridge — can create agent sessions, stream replies token-by-token over SSE, and continue conversations started in the Web UI, all behind API-key authentication. API sessions drive the same agent machine the GUI drives (inbox + session log), so both worlds stay in sync.

dsh plugin --profile web add dsh-api-gateway

Features

  • REST + SSE: 9 endpoints; token-level streaming (assistant/chunk), server closes the stream at turn_end
  • GUI settings card: Settings → Plugins → Configurable → API Gateway (status, soft on/off, key rotation)
  • Workspace membership: API sessions land in real workspaces and show grouped in the sidebar, never under "ungrouped"
  • Session discovery & adoption: list all sessions, read any session's full history (read-only), and adopt a GUI session to keep driving it over the API — live co-driving or cold resume with full context
  • Reasoning split: replies separate text (visible answer) from reasoning (thinking), never concatenated
  • Extensible: publishes gateway/session-created / gateway/message / gateway/turn-end on the Cordis event bus for other host plugins
  • Any language client: works from Linux/macOS/Windows, PowerShell included (UTF-8 aware, GBK-tolerant server side)

Install

# npm (prebuilt — no build approval needed)
dsh plugin --profile web add dsh-api-gateway

# straight from GitHub (prebuilt lib/ committed)
dsh plugin --profile web add github:litestartup-com/dsh-api-gateway

# from a packed tarball
dsh plugin --profile web add ./dsh-api-gateway-0.1.0.tgz

The built lib/ is committed, so GitHub installs need no build approval. Build scripts run only when packing or publishing (prepack).

Uninstall: dsh plugin --profile web remove dsh-api-gateway.

Manual composition row (no CLI)

The plugin is an ordinary Cordis row; you can also compose it by hand. It publishes a cross-session HTTP surface, so it belongs in the host composition (or the profile's patch layer) — never inside an agent preset:

- id: dsh-api-gw
  name: dsh-api-gateway
  config:
    prefix: /api-gw/v1          # route prefix
    enabled: true               # master switch (also toggleable at runtime)
    apiKeys: []                 # pre-provisioned static API keys
    allowKeyProvision: true     # one-time POST /key bootstrap
    adminKey: change-me         # enables admin endpoints + card controls
    maxSessions: 20             # concurrent session cap
    workspaceMode: auto         # auto (join a workspace) | ungrouped
    defaultWorkspacePath: ''    # fallback directory for auto mode
    allowDiscover: true         # GET /sessions/discover
    allowAdopt: true            # POST /sessions/:id/adopt
    corsOrigin: '*'             # '*' or an explicit origin / list
    exposeErrors: true          # include internal details in error responses
    sseHeartbeatMs: 30000       # SSE heartbeat interval (0 disables)
    bodyTimeoutMs: 30000        # request body read timeout

Every key has a schema default — see examples/cordis.yml for the annotated row.

Quick start

BASE=http://127.0.0.1:3080/api-gw/v1
KEY=$(curl -s -X POST $BASE/key | jq -r .apiKey)              # first call claims the key, once
SID=$(curl -s -X POST $BASE/sessions -H "Authorization: Bearer $KEY" | jq -r .sessionId)
curl -N $BASE/sessions/$SID/stream -H "Authorization: Bearer $KEY" &    # SSE
curl -s -X POST $BASE/sessions/$SID/messages \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"content":"你好,介绍一下你自己"}'

Windows PowerShell (no extra tools; UTF-8 safe):

$BASE = 'http://127.0.0.1:3080/api-gw/v1'
$KEY  = (Invoke-RestMethod -Method Post "$BASE/key").apiKey
$SID  = (Invoke-RestMethod -Method Post "$BASE/sessions" -Headers @{ Authorization = "Bearer $KEY" }).sessionId
$json  = '{"content":"你好,介绍一下你自己"}'
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
Invoke-RestMethod -Method Post "$BASE/sessions/$SID/messages" `
  -Headers @{ Authorization = "Bearer $KEY" } -ContentType 'application/json; charset=utf-8' -Body $bytes

PowerShell 5.1 sends ANSI/GBK by default → garbled Chinese. Use the UTF-8 byte form above (or declare charset=utf-8); PowerShell 7 is UTF-8 by default. The server honors the request Content-Type charset (default UTF-8, GBK-tolerant). No jq? brew install jq, or use the Python/PowerShell examples.

Python (httpx):

import httpx, json
base = "http://127.0.0.1:3080/api-gw/v1"
key = httpx.post(f"{base}/key").json()["apiKey"]
h = {"Authorization": f"Bearer {key}"}
sid = httpx.post(f"{base}/sessions", headers=h).json()["sessionId"]
httpx.post(f"{base}/sessions/{sid}/messages", headers=h, json={"content": "你好,介绍一下你自己"})
with httpx.stream("GET", f"{base}/sessions/{sid}/stream", headers=h) as r:
    for line in r.iter_lines():
        if line.startswith("data: "):
            print(json.loads(line[6:])["kind"])

Endpoints

MethodPathAuthDescription
GET/healthnoneStatus (reachable while disabled)
POST/keyfirst call onlyOne-time API key bootstrap
POST/sessionsAPI keyCreate a session (provider/model/maxTokens/cwd/workspace)
GET/sessions/discoverAPI keyList sessions (id/title/cwd/live/persisted) — no content
POST/sessions/:id/adoptAPI keyAdopt an existing session (live co-drive / resumed cold-resume); returns full history
POST/sessions/:id/messagesAPI keySend a message (string or block array)
GET/sessions/:id/streamAPI keySSE: hello(replay)→chunk→message→tool_call/tool_result→turn_end
GET/sessions/:id/historyAPI keyFull history of any session (read-only)
POST/sessions/:id/cancelAPI keyCancel the active turn
POST/admin/enableAdmin keyRuntime soft switch {"enabled": bool}
POST/admin/rotate-keyAdmin keyRotate the provisioned key

Auth headers, either form: Authorization: Bearer <key> (recommended, RFC 6750) or X-API-Key: <key>.

Full spec: openapi.yaml.

Security model

Why can POST /key just hand out a key? It's a first-call bootstrap, not an open mint:

  • Only when no key exists yet does POST /key generate a 32-char random key — exactly once. Afterwards the endpoint is locked (401 without a valid key).
  • By default the gateway listens on loopback, so the only possible "first caller" is you, the deployer — equivalent to setting a password at first boot.
  • Don't trust the window? Close it: allowKeyProvision: false, keys only from apiKeys: [...].

Defense in depth (production checklist):

  1. allowKeyProvision: false + pre-provisioned apiKeys
  2. Keep the gateway loopback-bound; put a reverse proxy + TLS in front if exposed
  3. Separate adminKey from API keys
  4. Per-session agent contexts; session ids are cryptographically random
  5. Authorization: Bearer as the canonical header (X-API-Key kept as an alias)
  6. Constant-time key comparison (crypto.timingSafeEqual), CSPRNG key generation

Known gaps (public, see roadmap): no per-key rate limiting/quotas, no revocation list, no multi-key management UI, no audit. For hostile multi-tenant scenarios wait for v0.2+, or front the gateway yourself. Holding an API key can discover/read/adopt all sessions — a feature for single-owner setups, a risk otherwise; disable via allowDiscover/allowAdopt (per-key allowlists land in v0.2.0).

Workspace membership

API sessions join workspaces just like GUI sessions — sidebar shows them grouped, never "ungrouped". POST /sessions accepts workspace in three forms:

{ "workspace": "C:\\projects\\team-a" }                                // path string
{ "workspace": { "path": "C:\\projects\\team-a", "title": "Team A" } } // + title on create
{ "workspace": { "id": "ws-xxx" } }                                    // existing workspace id

Rules (deterministic, server-side):

  • Path resolves to an existing workspace → reused; otherwise auto-created (title defaults to the basename)
  • Unknown id → 400 with the current workspace list (id/title/path)
  • No workspaceworkspaceMode: auto (default — resolve-or-create for the session cwd / defaultWorkspacePath) or ungrouped
  • Both cwd and workspace given → workspace wins; session cwd is forced to the workspace canonical path (the durable membership invariant: header cwd == workspace path)
  • Path pointing at a missing directory → 400 (the gateway never creates directories)

Responses and history include workspace: { id, path, title }. Shared collaborative workspaces (multiple keys on one path) arrive in v0.2.0.

Session discovery & adoption (continue GUI sessions over the API)

# ① discover sessions
curl -s $BASE/sessions/discover -H "Authorization: Bearer $KEY"

# ② adopt one: live co-driving, or cold resume; returns the full history
curl -s -X POST $BASE/sessions/$SID/adopt -H "Authorization: Bearer $KEY"
# → { "mode": "live" | "resumed", "history": [...] }

# ③ keep chatting — identical to gateway-created sessions
curl -s -X POST $BASE/sessions/$SID/messages \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"content":"continue where we left off"}'
modeMeaningLifecycle
createdGateway-created sessionOwned by the gateway
liveCo-driving a GUI-open session: API messages appear in the GUI flow, turns queue from both sidesBorrowed only — plugin stop just untracks it
resumedCold-resume of an offline session (needs sessionPersistence)Owned by the gateway after resume

GET /sessions/:id/history works for any session (read-only, no adoption needed); /messages, /stream, /cancel require adoption first.

vs the official Python SDK

DeepSeek Harness also ships an official Python SDK (tutorial / SDK reference). The two are not the same thing and not substitutes:

Official Python SDKThis gateway
NatureEmbedded runtime: pip install deepseek-harness-sdk ships a platform wheel and drives a bundled dsh-jsonrpc-agent subprocess over JSON-RPC stdioA door into a running Harness: a host-composition plugin exposing REST + SSE
Model credentialsDeepSeek API keys (DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL)Gateway's own API keys (independent of model credentials)
SessionsPrivate JSONL under session_root, unrelated to any deployment or GUIThe deployment's shared session corpus: GUI-visible, workspace-grouped, adoptable
CapabilitiesMinimal default composition (local bash etc., no skills, no compaction; customizable via cordis)The deployment's default agent preset (tools/skills/sandbox policy)
PlatformsLinux x64/arm64, macOS 14+ arm64; no WindowsAny client language/platform, Windows PowerShell included
Isolationdanger-full-access; run in disposable environments/containersInherits the deployment sandbox and approval policy
Best forOne-off isolated tasks from Python scripts without a long-running deploymentThird parties connecting to your running deployment, multi-language, unified auth/limits/audit, continuing GUI sessions

Choose the SDK for disposable Python tasks; choose this gateway for everything that needs a persistent, shared, cross-language door. Don't mix the two: DeepSeek sk-… keys don't open this gateway, and pip install deepseek-harness-sdk does not connect to it.

Extensibility (for other plugins)

The gateway publishes three events on the Cordis event bus; other host plugins subscribe with ctx.on(...) (listeners are fiber-owned and can never break the gateway):

  • gateway/session-created{ sessionId, mode: 'created' | 'live' | 'resumed', workspace, cwd }
  • gateway/message{ sessionId, messageId, text } (on each committed assistant reply)
  • gateway/turn-end{ sessionId, turn, reason, detail }

Typical uses: audit persistence, external alerting, forwarding to IM/webhooks, custom rate-limit sidecars.

Development & testing

pnpm install
pnpm build        # tsc
pnpm smoke        # end-to-end smoke against a running gateway

Smoke env: DSH_AGW_BASE (default http://127.0.0.1:3080/api-gw/v1), DSH_AGW_KEY (optional — claims a key if absent), DSH_AGW_PROMPT. CI (.github/workflows/ci.yml) runs build + syntax checks, with an optional smoke job activated by repository variables.

Roadmap

Milestones ordered by "security first, then experience, then ecosystem"; each version ships independently.

VersionThemeContents
v0.1.0Baseline (current)REST + SSE, settings card, reasoning/text split, workspace membership, session adopt, cross-platform docs
v0.2.0Multi-tenant security ★Multi-key CRUD/revocation, per-key rate limiting (429 + Retry-After), workspace model: per-key isolated + shared collaborative workspaces (shared/isolated), per-key approval policy, audit (requests/sessions/token usage per key), session persistence (resume after restart)
v0.3.0Admin UIFull admin settings page (keys/limits/workspace bindings, session monitor, usage audit, soft switch) + typert @Remote config surface (the admin page's foundation) + per-key agent preset selection
v0.4.0Duplex streamingwebServer.registerUpgrade WebSocket full-duplex (send/stream/cancel on one connection); SSE stays as the lightweight option
v0.5.0Ecosystem & opsPython/Node HTTP thin clients (OpenAPI-generated — not the official embedded SDK, see above), deployment guide (reverse proxy + TLS, Docker Compose), metrics/telemetry export, OpenAPI generation in CI

Out of scope / deferred: horizontal multi-process scaling, built-in TLS termination (a reverse proxy's job), OAuth/OIDC (revisit after the key-based model settles).

License

MIT

Project files and signals

Shown items are public repository signals detected in the directory snapshot.

ExamplesDetected

Repository information

Language
JavaScript
License
MIT
Last updated
Aug 16, 2026, 6:22 PM

Install deliberately

Review source code, permissions, lifecycle hooks, dependencies and network access. Test untrusted plugins in an isolated environment.