DeepSeek Harness Plugin

litestartup-com/dsh-api-gateway

Stars ★ 2 Category Development & Runtime Added 2026-08-16

REST + SSE gateway exposing running Harness sessions to third-party clients: API-key auth, token streaming, workspace grouping, and adopting GUI sessions to keep chatting.

Install

# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)

dsh plugin --profile web add github:litestartup-com/dsh-api-gateway

Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).

README

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

Recommended: dsh plugin add

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

# straight from GitHub (sources)
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

Git installs fetch sources and run this package's prepare (build) script. pnpm ≥ 10 refuses that until you allow it: copy the exact package key pnpm prints into the profile's pnpm-workspace.yaml under allowBuilds and re-run the add. That allowance means "execute this package's code at install time" — only allow sources you trust, and pin a commit when in doubt.

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: api-gateway
  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

Method Path Auth Description
GET /health none Status (reachable while disabled)
POST /key first call only One-time API key bootstrap
POST /sessions API key Create a session (provider/model/maxTokens/cwd/workspace)
GET /sessions/discover API key List sessions (id/title/cwd/live/persisted) — no content
POST /sessions/:id/adopt API key Adopt an existing session (live co-drive / resumed cold-resume); returns full history
POST /sessions/:id/messages API key Send a message (string or block array)
GET /sessions/:id/stream API key SSE: hello(replay)→chunk→message→tool_call/tool_result→turn_end
GET /sessions/:id/history API key Full history of any session (read-only)
POST /sessions/:id/cancel API key Cancel the active turn
POST /admin/enable Admin key Runtime soft switch {"enabled": bool}
POST /admin/rotate-key Admin key Rotate 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"}'
mode Meaning Lifecycle
created Gateway-created session Owned by the gateway
live Co-driving a GUI-open session: API messages appear in the GUI flow, turns queue from both sides Borrowed only — plugin stop just untracks it
resumed Cold-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 SDK This gateway
Nature Embedded runtime: pip install deepseek-harness-sdk ships a platform wheel and drives a bundled dsh-jsonrpc-agent subprocess over JSON-RPC stdio A door into a running Harness: a host-composition plugin exposing REST + SSE
Model credentials DeepSeek API keys (DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL) Gateway's own API keys (independent of model credentials)
Sessions Private JSONL under session_root, unrelated to any deployment or GUI The deployment's shared session corpus: GUI-visible, workspace-grouped, adoptable
Capabilities Minimal default composition (local bash etc., no skills, no compaction; customizable via cordis) The deployment's default agent preset (tools/skills/sandbox policy)
Platforms Linux x64/arm64, macOS 14+ arm64; no Windows Any client language/platform, Windows PowerShell included
Isolation danger-full-access; run in disposable environments/containers Inherits the deployment sandbox and approval policy
Best for One-off isolated tasks from Python scripts without a long-running deployment Third 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.

Version Theme Contents
v0.1.0 Baseline (current) REST + SSE, settings card, reasoning/text split, workspace membership, session adopt, cross-platform docs
v0.2.0 Multi-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.0 Admin UI Full 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.0 Duplex streaming webServer.registerUpgrade WebSocket full-duplex (send/stream/cancel on one connection); SSE stays as the lightweight option
v0.5.0 Ecosystem & ops Python/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

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →