返回目录
其他 插件

dsh-api-gateway

litestartup-com/dsh-api-gateway

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

Stars
11
Forks
2
Issues
0
更新
3 天前

PROJECT TOPICS

项目标签

INSTALL REFERENCE

安装参考

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

该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。

PROJECT README

README

ohdsh-api-facade

A DeepSeek Harness host plugin: an authenticated, fail-closed in-process HTTP facade that serves the host's session surface (the 0.1.2 typertGateway remotes + follow/control streams + question/approval waterfalls) to remote clients under the frozen legacy apiproxy contract (typical client: dsh-agent-manager).

The plugin does exactly three things: authentication, whitelisting, and contract translation. The 0.1.1-era loopback HTTP forwarding was removed with the 0.1.2 rebuild; envelopes, frame shapes, and receipts remain identical to 0.1.1 — every version difference is absorbed here, so clients need zero changes.

Why it exists

DSH's /api surface sits behind a two-layer gate (trust fence + browser auth) that remote clients cannot pass, and its in-process seams are not exported. This plugin runs inside the DSH process and calls the host domain services directly (typertGateway dispatcher, session streams, waterfalls), guarded by API-key auth and a deny-by-default whitelist.

Supported DSH versions

The facade runs inside a DSH host, so its compatibility surface is the host version. The declared range lives in package.json peerDependencies (^0.1.2-rc.1 || ^0.2.0-0 — dual-range since facade 0.2.4: the 0.2.0 corridor broke the old ceiling, and the DSH host enforces peers at install and startup); the pairings below are the ones verified end-to-end — wire contract, question/approval card chains, and GUI token capture — not merely semver-declared:

DSH Status Evidence
0.2.0-rc.2 ✅ verified Full-chain smoke + question/approval card chains + V3→V4 session-volume migration on the standalone Docker stack (facade 0.2.4, host 192.168.33.11); npm latest line
0.1.5-rc.2 ✅ verified Full-chain + card-chain smoke on facade 0.2.4 (dual-compat regression of the 0.2.0 corridor work) and on #b592b4f before it; see the install note below
0.1.2-rc.1 ✅ verified Full-chain smoke (dsh-agent-manager/scripts/smoke-proxy-b.ts, real model turn)
0.1.1-rc.2 ⚠️ legacy Wire contract frozen from this era; not the supported base

0.1.5 install note: npm's strict peer resolution rejects the default install (ERESOLVE) even though the range covers 0.1.5 semantically — compose 0.1.5 profiles with npm install --legacy-peer-deps. Consumers track this in their version matrix (dsh-agent-manager src/dsh-matrix.ts, needsLegacyPeerDeps). The same posture applies to the 0.2.0 line.

0.2.0 corridor note (what facades 0.2.4/0.2.5 absorb, so clients don't have to): the host's wireStream.open grew duplex uplink/peer parameters (arity-based detection keeps one code path booting both host generations); ctx.settings.register is gone host-side, so the durable key path on 0.2.0 hosts is the composition config (the Docker stack's entrypoint injects GW_KEY into the profile patch; POST {prefix}/key bootstrap keys are memory-only there); session logs moved to V4 (V3 volumes are migrated on read — one-way, back up before upgrading); the DeepSeek session-log upload defaults to on (the Docker stack pins it off in the profile patch); and streaming deltas moved out of the durable log into an opt-in live channel — since 0.2.5 the facade subscribes to it and re-emits the frozen assistant/chunk frames, so typewriter clients keep streaming with zero changes. The wire contract itself is unchanged — managers and probes pinned to older facade commits keep working against 0.2.0 hosts through this facade.

Consumers pin the facade by commit (github:litestartup-com/dsh-api-gateway#<sha>), so each DSH line is re-verified before a pin moves; verification records live in the private design library (dsh-facts). The 0.1.6-alpha.* / 0.1.7-* lines were superseded by 0.2.0 and are not separately verified (the 0.2.0 corridor spans them via the community jump cards).

Install

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

Add one row to the host composition (see examples/cordis.yml) and restart DSH.

Naming note: DSH ships a built-in package named @deepseek-ai/dsh-api-gateway (the typert dispatcher) which is unrelated to this plugin. This plugin is the external HTTP facade; its settings namespace / composition row / service field are all ohdsh-api-facade.

Docker deployment (standalone API stack)

The repo ships a self-contained compose stack that runs the facade as a standalone, public-facing API service — no manager and no extra wiring required:

client ──HTTP──▶ nginx (:${HTTP_PORT}) ──/api-gw/ only──▶ gateway container
                                                          = DSH host + this facade

The gateway port is never published; nginx is the only front door and it is fail-closed: only /api-gw/ is proxied, everything else (the DSH web GUI, /api, assets) answers 404. Behind that door the facade enforces its own API-key auth and the deny-by-default method whitelist — two independent layers.

Quickstart

bash docker/gen-env.sh          # generate .env (HOST_UID/GID, a random GW_KEY) — idempotent
# edit .env: fill in DEEPSEEK_API_KEY (required for real session turns)
docker compose up -d --build    # builds the node image (pinned DSH + committed lock) and boots
node docker/smoke.mjs           # wiring acceptance; add --model for one real model turn
node docker/probe-cards.mjs     # question/approval card chains (respond roundtrips; needs a model key)

The API base becomes http://<host>:${HTTP_PORT}/api-gw/v1, authenticated with GW_KEY from .env as X-API-Key:

curl -s http://127.0.0.1/api-gw/v1/health
curl -s -X POST http://127.0.0.1/api-gw/v1/proxy/session.list \
  -H "X-API-Key: $GW_KEY" -H 'content-type: application/json' \
  -d '{"type":"client-request","rpcId":"1","method":"session.list","payload":{}}'

Files

Path Role
docker-compose.yml nginx + gateway services, health-gated startup
docker/Dockerfile one container = one DSH API node (pinned DSH + the facade from this checkout)
docker/gen-profile.mjs build-time profile generator (lock-driven npm ci; --lock-only refreshes the lock)
docker/profile-lock/ committed dependency locks (reproducible trees, one per DSH version)
docker/entrypoint.sh idempotent seeding: profile → volume, GW_KEY → settings.yaml
docker/nginx/gateway.conf the fail-closed front door (API prefix only; WebSocket-upgrade aware)
docker/gen-env.sh .env generator (HOST_UID/GID red line, random GW_KEY)
docker/probe-lib.mjs shared probe machinery (config, envelopes, raw-WS client)
docker/smoke.mjs zero-dependency stack acceptance (includes a raw-WS mux check)
docker/probe-cards.mjs question/approval card-chain probe (respond roundtrips + sandbox escalation + file-on-disk proof)

.env reference

Variable Default Description
HTTP_PORT 80 External nginx port (plain HTTP; TLS is not wired in v1)
GW_KEY generated Static facade API key (X-API-Key). Empty leaves the one-time POST /key bootstrap open — not recommended on a public surface
DEEPSEEK_API_KEY — Official DeepSeek route credential; required for real turns unless an OpenAI-compatible route below is configured
OPENAI_BASE_URL — Optional: activates the OpenAI-compatible route (one URL, a group of models — see below)
OPENAI_API_KEY / OPENAI_PROVIDER / OPENAI_MODELS / OPENAI_MODEL / FACADE_MODEL see below Route credential (optional), key (default openai), model list, single-model shorthand, default model (provider/model)
OPENAI_API / OPENAI_CONTEXT_WINDOW / OPENAI_MAX_TOKENS see .env.example Optional protocol / capacity refinements
DSH_VERSION 0.2.0-rc.2 Pinned DSH line baked into the image (needs a matching docker/profile-lock/ entry; 0.1.5-rc.2 remains supported)
NGINX_IMAGE nginx:alpine Override where alpine cannot be pulled (e.g. docker.m.daocloud.io/library/nginx:alpine)
NODE_IMAGE / NPM_REGISTRY docker.io / npmjs Build-time mirrors for GFW builds
HOST_UID / HOST_GID 1000 Container runtime uid = host file-owner uid (written by gen-env.sh)
GW_ADMIN_KEY — Optional: enables the {prefix}/admin/* endpoints
GW_ALLOW_FULL_ACCESS — Optional true: the sandbox route may grant danger-full-access (risk notice under Configuration)
GW_EXPOSE_ERRORS — Optional false: strip internal error details (recommended for public deployments)
GW_CORS_ORIGIN — Optional: tighten CORS origin(s) for public deployments

Sessions & workspaces

./workspaces on the host is mounted at /workspace in the gateway container. When creating a session through the API, pass a cwd under that mount (e.g. /workspace/my-project) — the same tree is visible as ./workspaces/my-project on the host. DSH state (settings, credentials, session logs) lives in the gateway-data named volume and survives docker compose down; down -v wipes it.

OpenAI-compatible LLM route

The node image ships DSH's built-in llm-pi-ai adapter (the pi-ai multi-provider bridge). OPENAI_BASE_URL in .env activates the route — one URL, a group of models, no image rebuild, one docker compose up -d away:

# .env — any OpenAI-compatible endpoint (OpenAI, DeepSeek's /v1, vLLM, OneAPI, Ollama…)
OPENAI_BASE_URL=https://api.openai.com/v1   # up to, not including, /chat/completions
OPENAI_API_KEY=***                          # optional — omit for key-less endpoints
OPENAI_MODELS=gpt-4o-mini,gpt-4o            # comma-separated; the group this URL serves
FACADE_MODEL=openai/gpt-4o-mini             # default for new sessions (provider/model)
docker compose up -d                        # recreates the gateway; config regenerates at boot
node docker/smoke.mjs --model --provider openai   # asserts the route served the turn

The variable vocabulary is shared with the sibling pi-api-facade project (which keeps its historical PI_OPENAI_* names as aliases of the same OPENAI_* set), so one operator — or one DAC manager wiring layer — feeds a Pi node and a DSH node the same model configuration. Both facades sit on the same pi-ai layer underneath, so a provider/model string means the same thing on both.

Behavior facts (source-verified; private design library dsh-facts §20):

  • OPENAI_PROVIDER (default openai) is the route key — the provider value visible in session.models and request/header events. The default key overrides the endpoint of pi-ai's built-in openai catalog route, so its model ids come free; a custom key declares a brand-new route, which should list its models explicitly (OPENAI_MODELS).
  • One honest difference vs the pi side: an explicit OPENAI_MODELS list replaces the route's catalog here, where pi adds to it. Practical effect is the same — against a custom gateway you list what it actually serves.
  • Wire protocol: OPENAI_API defaults to openai-completions (OpenAI Chat Completions; pi-ai also serves openai-responses, anthropic-messages, google-vertex, …). Capacity claims are configuration's job — nothing interrogates a gateway: OPENAI_CONTEXT_WINDOW (default 262144) and OPENAI_MAX_TOKENS (default 32768) are the route-level fallbacks for models that declare none.
  • The key never lands in a config file: the profile stores apiKeyEnv: OPENAI_API_KEY (the variable name) and DSH's credential layer resolves the process environment per request (env is its top layer). A key-less endpoint simply leaves OPENAI_API_KEY empty.
  • FACADE_MODEL (provider/model, same contract as the pi side's PI_FACADE_MODEL) picks the default for new sessions; unset, the first declared model wins, and with no models declared the bundle default (deepseek-official/deepseek-flash) stays. It also works standalone — without any OPENAI_* — to re-pin the default to any mounted route.
  • Every mounted route stays available: all of them appear in session.models, and session.selectModel pins an individual session to any of them. With no OPENAI_* at all, DEEPSEEK_API_KEY remains the default path — existing deployments change nothing.
  • Requires a 0.2.x DSH line (the stack default). Legacy 0.1.2/0.1.5 images ignore OPENAI_* with a boot warning.

Upgrades & lock refresh

docker compose up -d --build rebuilds the image from the current checkout. The entrypoint re-seeds the profile into the volume whenever the image's seed version changes (DSH pin, facade version, or plugin content) — no manual step, and .env's GW_KEY remains the source of truth for the key.

Refresh the committed lock first whenever DSH_VERSION or the facade's dependency ranges change:

node docker/gen-profile.mjs --lock-only 0.2.0-rc.2
# → writes docker/profile-lock/0.2.0-rc.2.package-lock.json — commit it

Debug note: the DSH web GUI is not exposed. If you need it, uncomment the loopback mapping in docker-compose.yml (127.0.0.1:3081:3080) and reach it through an SSH tunnel — never on a public surface.

Web demos

examples/demos/ ships two web demos on top of this stack (a compose overlay adds them behind the same nginx door): KB Studio (/kb/ — knowledge-base management with an AI steward session pinned to workspace-write) and a Support widget (/cs/ — a read-only support agent grounded in the same knowledge base, which is this project's own documentation). Both are zero-dependency BFF + vanilla-JS apps and double as reference clients for the wire contract — see examples/demos/README.md.

Configuration

Field Default Description
prefix /api-gw/v1 Route prefix
enabled true Master switch (also toggleable via the admin endpoint)
apiKeys [] Static API keys
provisionedKey — Key minted by POST {prefix}/key, persisted in settings
allowKeyProvision true Allow the one-time unauthenticated key bootstrap
adminKey — Enables the admin endpoints when set
corsOrigin * CORS origin(s)
exposeErrors true Include internal error details in responses
allowFullAccess false Allow the sandbox route to grant danger-full-access (risk notice: operator opt-in; the gateway logs a warning at boot and on every hit, and applies no environment restriction)
proxyWhitelist default list Optional whitelist override

proxyTarget is kept only for 0.1.1-era config compatibility (deprecated; its value no longer takes part in any request path).

Endpoints

Method Path Auth
GET {prefix}/health none
POST {prefix}/key first call only (one-time bootstrap)
POST {prefix}/admin/enable X-Admin-Key
POST {prefix}/admin/rotate-key X-Admin-Key
POST {prefix}/proxy/<method> X-API-Key / Bearer
POST {prefix}/respond and {prefix}/proxy/respond X-API-Key / Bearer
POST {prefix}/sessions/{id}/sandbox-mode X-API-Key / Bearer
GET {prefix}/events.mux (WebSocket upgrade) X-API-Key

POST {prefix}/proxy/<method> takes a client-request envelope and returns a server-response envelope (HTTP is always 200; success lives in result.ok). Methods served in-process: session.list / session.create / session.prompt / session.cancel / session.history (follow-stream snapshot translation) / session.rename / session.fork / session.updateQueue / session.attachment / session.models (modelCatalog translation) / session.selectModel / host.describe (synthesized protocol constant 0.0.1, DSH-FACTS §6). Anything whitelisted but not migrated answers 501 method_not_migrated — the facade never silently forwards down a dead path.

sessions/{id}/sandbox-mode pins { "mode": "read-only" | "workspace-write" } (danger-full-access requires allowFullAccess: true) on a live session by writing a durable sandbox/mode log event. Cold/unknown sessions → 409 session_not_live. This is the only wire channel that can pin a session's sandbox mode (used once, right after creation).

respond uses the legacy { accepted, reason? } receipts; question claims / declines (ASK_CANCELLED semantics) / approval outcomes behave exactly as 0.1.1.

The mux upgrade path is also registered at {prefix}/proxy/events.mux, so clients with the uniform "base + method" convention (the manager's rpc base is /api-gw/v1/proxy) need no special case. The mux pipe is downlink only (clients sending frames are closed with 1008). Live traffic = per-session follow streams (session events) + one host-wide control stream (live projections as per-key session/projection frames). Reconnection is the client's job.

Default whitelist

session.list, session.create, session.history,
session.prompt, session.cancel, session.rename,
session.fork, session.updateQueue, session.attachment,
session.models, session.selectModel,
respond,  host.describe

Anything else → 403 { error: 'method_not_allowed' } without touching host services. The privileged plane (credentials.*, settings.*, host.openPath, host.pickDirectory, llm.discoverModels, …) is unreachable through the facade. Note the real method name is host.describe (host.version does not exist).

Security model

  • Non-degradable auth: constant-time key comparison, CSPRNG keys, one-time key bootstrap (closes permanently once any key exists).
  • Fail-closed whitelist; unmigrated methods answer 501 honestly — there is no path that silently forwards to a dead channel.
  • respond only settles pending entries this plugin itself forwarded (rpcId match); anything unknown is not-pending.
  • Keys are never logged; apiKeys/adminKey are redacted on the settings wire surface.

Deploy steps

  1. Build and commit: pnpm build && pnpm test (all green; lib/ must be committed).
  2. Update the host install: dsh plugin update (or pnpm install under profiles/web).
  3. Restart DSH.
  4. Run acceptance (below).

Acceptance

  1. GET {prefix}/health → 200, upstream: ok.
  2. POST {prefix}/proxy/credentials.set (with a valid key) → 403 method_not_allowed.
  3. POST {prefix}/proxy/session.list with a wrong key → 401.
  4. With a valid key: POST {prefix}/proxy/host.describe → { version: '0.0.1' }; session.list returns the session list.
  5. WebSocket to ws://host{prefix}/proxy/events.mux (with X-API-Key in the handshake): after session.prompt you should see session/event frames up to turn/end, with projection updates streamed as session/projection frames.

Automated acceptance: dsh-agent-manager/scripts/smoke-proxy-b.ts (the manager's end-to-end smoke over the proxy path, including a real model turn).

Uninstall

Remove the plugin row from the composition (optionally dsh plugin remove ohdsh-api-facade) and restart.

Documentation scope

This repository ships only what consumers need: this README, README.zh.md, openapi.yaml, examples, and tests. Internal design and the refactor plan live in a private design library — the code, the wire contract, and the examples are the complete, runnable, self-hostable deliverable.

License

MIT

CLASSIFICATION EVIDENCE

分类依据

项目类型插件
功能分类其他
规则置信度低

系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。