deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
PROJECT README
--host (non-invasive bundle)dsh web originally restricts the bind host:
dsh-web-app/lib/startup.js — rejects --host 0.0.0.0 outright (usage error).dsh-host-webserver/lib/index.js — the webServer config schema only
accepts the literals 127.0.0.1 or 0.0.0.0, so no other host (e.g. a
concrete LAN IP) can be configured.dsh-web-app/lib/index.js (resolveLanTrust) — the /api browser-trust
fence auto-derives trusted authorities only for the 0.0.0.0 bind.This bundle removes those restrictions: any --host value is accepted
verbatim and handed to node:http server.listen, and the app refuses to
load from a non-secure browser origin instead of booting half-broken.
The global dsh installation is untouched (wherever it lives — the installer
locates it dynamically, see below). Instead, a single bundle package plugin/
is registered as an extra bundle layer of the web profile through the
official mechanism:
dsh plugin --profile web add ./plugin
That runs pnpm add in the profile and reconcilePlugins appends
dsh-arbitrary-host to dsh.profile.bundles (now
[@deepseek-ai/dsh-base, @deepseek-ai/dsh-web-app, dsh-arbitrary-host]).
During local development the dependency is a pnpm link: — the plugin dir
stays the single source of truth, so edits are live (no reinstall for code
changes).
The bundle's cordis.patch.yml uses the include patch dialect:
the shipped web-startup / webserver / web-runtime / connection rows
are disabled and replacement rows are inserted under new ids. (The
dialect cannot change a row's name — "name mismatch, skipping" — so disable
The connection replacement row is named with the bare package name
(name: 'dsh-arbitrary-host', like dsh-webui-auth): its main export is the
connection implementation and its ./client export is the restamped client
bundle — so a single package carries both the server and browser halves.
dsh updates never touch the profile, and nothing in the bundle
edits a shipped file, so an update cannot "lose" these changes.webserver.js
subclasses the shipped WebServer (only the host schema is widened),
frontend-static.js reuses the shipped serveStatic, and glue.js
reuses the shipped webAppInternals.resolveDistIndex. When the global
packages update, the new code flows into the bundle automatically.dsh web --dump-config). The plugin's
node_modules symlink (below) must point at the current global install.plugin/lib/startup.js — shipped startup minus the --host 0.0.0.0
rejection (only the non-numeric --port check remains).plugin/lib/webserver.js — WebServer subclass with
host: z.string().required() (any host string reaches server.listen).plugin/lib/glue.js — web-runtime glue mounting the gated
frontend-static, with resolveLanTrust also deriving a concrete
non-loopback IP-literal bind host as a trusted /api authority.
Hostname binds stay caller-declared (--trusted-host); the fence's
loopback and cross-site checks are untouched.plugin/lib/frontend-static.js — secure-context gate: the Web shell
depends on crypto.randomUUID, which browsers expose only in secure
contexts (HTTPS, or localhost). Over plain HTTP on a LAN IP or hostname the
app would boot half-broken (lists fail, "add workspace" throws
crypto.randomUUID is not a function), so the fallback returns a
self-contained 403 guidance page (Chinese/English) instead, pointing to:
loopback on the host, an HTTPS reverse proxy (X-Forwarded-Proto: https
passes the gate), or a browser "insecure origins treated as secure" flag.
/api and /plugins routes are untouched, so scripts/curl keep working.plugin/lib/connection.js — connection row replacement (row name
dsh-arbitrary-host)@deepseek-ai/dsh-client-connection pins a set of methods
(settings.*, credentials.*, agentPreset.*, host.pickDirectory,
host.openPath, llm.discoverModels) to loopback only by design.
Over a reverse-proxy domain these returned
transport failure for /api/settings.describe: HTTP 403.HostConnectionService, toFetchHandler, fence, bridge, and WebSocket
downlinks; the browser client bundle is a copy of the shipped one with
only the registration id restamped, re-synced by the installer), with
the loopback-only gate driven by a new row config
privilegedMethods (default [] = none privileged).privilegedMethods: [] every method still passes the outer
DNS-rebinding fence (trustedHosts), so only loopback or a declared
--trusted-host authority reaches the configuration plane;
un-declared hosts stay 403 (verified). Relaxing the plane is the
deployment's deliberate choice — trustedHosts is explicitly not
authentication. Re-lock specific methods by listing them in
privilegedMethods in cordis.patch.yml.plugin/ the single bundle package (pnpm link: dep of the web profile)
package.json name, exports map, dsh.bundle.patch + dsh.client manifest
cordis.patch.yml disable shipped rows + insert replacements
lib/startup.js web-startup replacement
lib/webserver.js WebServer subclass (relaxed host schema)
lib/frontend-static.js gated SPA server (reuses shipped serveStatic)
lib/glue.js web-runtime replacement (LAN trust + gate mount)
lib/connection.js connection row replacement (row name = the package name)
lib/client.js restamped client bundle — GENERATED by install.mjs
(see below; deliberately not committed)
node_modules -> global install (lets the plugin's own imports resolve;
local link: development only)
install.mjs installer — registers the package via `dsh plugin` (idempotent, Node.js only)
uninstall.mjs uninstaller — removes the package again (idempotent, Node.js only)
lib/client.js is generated, not committedThe connection row replaces the shipped @deepseek-ai/dsh-client-connection
row, so its browser-half bundle must carry a different registration id. The
bundle is a copy of the shipped client with only that id rewritten — ~10k
lines of vendored shipped code. Committing it would make the repo carry a
stale-by-construction copy (it must track every global dsh update), so the
installer regenerates it from the CURRENT global install on every run.
Consequence: a fully remote one-liner (dsh plugin add github:...) is not
possible for the connection row — clone the repo and run node install.mjs
instead (it performs the official dsh plugin add under the hood).
The installer is idempotent (Node.js only, no shell script — and no pnpm requirement):
node install.mjs # Node.js only — no shell script, pnpm optional
dsh web --dump-config | grep arbitrary-host # confirm the rows
Registration prefers the official dsh plugin mechanism when pnpm is on
PATH (it also maintains the profile's pnpm-lock.yaml); when pnpm is
absent the installer falls back to an equivalent pure-Node registration
(profile manifest dependency + bundle layer + node_modules link — the same
state dsh plugin add produces; the next dsh plugin/pnpm run in the
profile reconciles the lockfile from the manifest). uninstall.mjs mirrors
this. The loader and the boot never read the pnpm lockfile, so both paths
produce a profile that boots identically (verified).
Both honor DSH_PROFILE (default web) and DSH_GLOBAL_NODE_MODULES
(override for the global node_modules). No machine-specific path is
hardcoded anywhere.
The installer needs the global node_modules that holds the shipped
@deepseek-ai packages (to sync the client bundle and to let the plugins'
imports resolve). It is found regardless of how dsh was installed, using
only Node built-ins (no sh, no npm/pnpm CLI probes — cross-platform):
DSH_GLOBAL_NODE_MODULES, if set;dsh binary on PATH resolves to — realpath the
binary, then walk up probing both each directory and its node_modules
(the second probe finds package-own dependency trees, e.g. the
.../@deepseek-ai/dsh/node_modules layout mise/pnpm produce);dirname(process.execPath)/../lib/node_modules and friends), which
covers mise / nvm / npm layouts even when dsh is not on PATH;$BUN_INSTALL/install/global/node_modules, then
npm-style prefix roots (best effort).Only roots that actually hold the shipped
@deepseek-ai/dsh-client-connection/lib/client.js are accepted, and the
chosen root is probe-checked for compatibility: if its own
@deepseek-ai/dsh-host-apiproxy calls z.looseObject, its zod must export
it, or the installer aborts with a clear message. (This is the failure the
probe exists for: a wrong root — e.g. an unrelated tool's global install
carrying @deepseek-ai packages as transitive dependencies with a different
zod — makes the very first boot die with
z$1.looseObject is not a function when the connection row is imported.)
If none matches, it prints where it looked and how to set the override.
The dsh plugin command is the entire official mechanism; the only extra
step is the client-bundle sync, which needs the global path from which dsh:
# .../node_modules/@deepseek-ai/dsh/lib/bin.js -> up four levels to node_modules
GLOBAL="$(dirname "$(dirname "$(dirname "$(dirname "$(readlink -f "$(command -v dsh)")")")")")"
dsh plugin --profile web add ./plugin
cp "$GLOBAL/@deepseek-ai/dsh-client-connection/lib/client.js" plugin/lib/client.js
sed -i 's|id: "@deepseek-ai/dsh-client-connection"|id: "dsh-arbitrary-host"|' plugin/lib/client.js
($GLOBAL resolves to .../node_modules for bun/npm symlinked bins; for
other layouts set it to the root that contains @deepseek-ai.)
node uninstall.mjs # idempotent; restores the shipped rows automatically
uninstall.mjs removes the package from the profile's
dependencies (dsh plugin ... remove reconciles dsh.profile.bundles), so
the shipped web-startup / webserver / web-runtime / connection rows
come back on the next boot, and clean the node_modules symlinks / sync
marker that the installer created inside the plugin dir. Your source files
are never touched.
dsh web --host 0.0.0.0 # all interfaces
dsh web --host 192.168.1.5 # one LAN address (auto-trusted for /api)
dsh web --host myhost.local --trusted-host myhost.local # name bind: declare it
dsh web --port 0 # OS-assigned port (unchanged)
Reverse proxy: forward the original Host (bare hostname) and set
X-Forwarded-Proto: https, and start dsh with
--trusted-host <public-hostname> — otherwise the /api browser-trust fence
rejects the proxied hostname (transport failure ... HTTP 403). The
dsh-arbitrary-host-connection row also relaxes the loopback-only
configuration plane (settings.*, credentials.*, agentPreset.*, ...) to
declared trusted authorities; un-declared hosts remain 403.
LAN access caveat: the plain-HTTP LAN origin shows the 403 guidance page
by design (see frontend-static.js).
MIT — see LICENSE in this directory.
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。