Files
deepseek-harness/README.md
T
Miles WardandClaude Fable 5 2d033c323b
Sync upstream and build images / sync-and-build (push) Successful in 2m4s
Post-incident hardening: pnpm, pinned versions, immutable tags, no more :latest
- Dockerfile: install pnpm (pinned via PNPM_VERSION arg) alongside dsh so
  'dsh plugin --profile web add|update|remove' works in-container. Plain
  'npm i -g' instead of corepack: corepack shims resolve versions at runtime
  and write to a root-owned cache, breaking the uid-1000/read-only contract.
  All pnpm writable dirs (store via PNPM_HOME, cache/state via XDG vars)
  point into the /home/node/.dsh volume - verified zero rootfs writes.
- Dockerfile: ENTRYPOINT ['dsh'], CMD carries the patch flag AND --no-open
  (a headless server must not open a browser; on 0.1.2+ that can burn the
  one-time launch token). DSH_VERSION default pinned to 0.1.1-rc.2.
- CI: never publishes or moves 'latest' (deleted from the registry). Builds
  only missing immutable version tags; a toolchain freshness check rebuilds
  the pinned version as <ver>-rN when a newer pnpm or node base image ships.
  Every build passes a smoke test before push: dsh/pnpm versions match and
  the server boots with the baked patch, answering on the container's
  non-loopback interface (200 pre-token, 303/401 on 0.1.2+).
- README: operating contract - pinned version rationale, PostArgs-replace-CMD
  warning, 0.1.2 launch-token behavior, plugin management, lanmode
  incompatibility, template pinned to :0.1.1-rc.2.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-09-06 02:43:10 -04:00

11 KiB

DeepSeek logo

deepseek-harness — trusted mirror & container build

Mirror of the official deepseek-ai/deepseek-harness with a locally-built, auditable container image for homelab use (Unraid).

Branch layout

Branch Contents
master Exact mirror of upstream master (plus all dsh-v* tags). Never edited locally.
main This branch: Dockerfile, patch file, and the sync/build workflow.

Image

gitea.milesward.dev/mward4/deepseek-harness:<version>        e.g. 0.1.1-rc.2
gitea.milesward.dev/mward4/deepseek-harness:<version>-r<N>   toolchain refresh of the same version

Built from node:22-bookworm-slim + the official @deepseek-ai/dsh npm release, pinned to the exact version. Upstream publishes no container image; this repo exists so the image provenance is: upstream tag → upstream npm release → this Dockerfile → this registry.

There is no :latest tag, deliberately. This deployment once tracked latest; a routine restart silently moved it from dsh 0.1.1-rc.2 to the 0.1.2 line, which introduced launch-token auth and replaced the ApiProxy with the @Remote gateway (breaking the lanmode plugin). Every tag here is immutable and every upgrade is an explicit template edit.

Pinned default: 0.1.1-rc.2 (ARG DSH_VERSION in the Dockerfile) — the last version verified against this deployment: no launch-token auth, and @goodandready/dsh-lanmode is compatible. Newer versions get their own tags built by CI (see below) but nothing repoints until you edit the template.

No upstream code is modified. Upstream deliberately refuses --host 0.0.0.0 on the CLI (the web UI can execute code, so exposing it to a network is a deliberate opt-in); the supported opt-in is a --patch launcher layer, which this image ships at /opt/deepseek-harness/web.cordis.patch.yml (bind 0.0.0.0:3080). The UI is reachable only through ports you explicitly publish, and the /api trust fence still refuses any Host it doesn't know.

Paths inside the container:

Path Purpose
/home/node/.dsh dsh state: settings, credentials, sessions, plugins (DSH_HOME)
/workspace The agent's working directory (container WORKDIR)
/opt/deepseek-harness/web.cordis.patch.yml Baked web-server bind patch

Runs as user node (UID/GID 1000). Everything writable at runtime — dsh state and pnpm's store/cache for plugin installs — lives inside the /home/node/.dsh volume, never the rootfs.

Command contract

ENTRYPOINT ["dsh"]
CMD ["web", "--patch", "/opt/deepseek-harness/web.cordis.patch.yml", "--no-open"]
  • --patch must come immediately after web (it's a launcher flag; after the web command's own flags it fails with "unknown option").

  • --no-open is mandatory on a server: on 0.1.2+ an in-container browser launch can consume the one-time launch token before you do.

  • Unraid "Post Arguments" REPLACE this CMD entirely. If you set Post Arguments you must repeat the whole command yourself:

    web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open --trusted-host <host> [--trusted-host <host2>]
    
  • --trusted-host is deployment-specific and is never baked into the image.

Plugin management

pnpm ships in the image (dsh's plugin manager requires it), so plugin operations work through dsh's own tooling against the state volume:

docker exec <container> dsh plugin --profile web add    <pkg>
docker exec <container> dsh plugin --profile web update <pkg>
docker exec <container> dsh plugin --profile web remove <pkg>

Plugins live in the runtime volume (/home/node/.dsh/profiles/web), not in the image — image rebuilds never touch them.

dsh 0.1.2+ launch-token auth (read before upgrading)

From 0.1.2, the Web UI requires a one-time launch token:

  • The tokened URL is printed in the startup log: docker logs <container>.
  • The token rotates on every restart of the container.
  • Opening the tokened URL exchanges it for a ~30-day cookie bound to the exact origin you opened it on — do the exchange on the hostname you actually intend to use (e.g. your tunnel domain, not the LAN IP).
  • Bare requests without token/cookie get "dsh web authentication required".

Known incompatibility: @goodandready/dsh-lanmode does not work with dsh 0.1.2+ (the @Remote gateway replaced the ApiProxy it monkey-patches; symptom: "web boot: 26 entries did not activate ... waiting for services: remote.*"). Remove or update it before moving to 0.1.2: docker exec <container> dsh plugin --profile web remove @goodandready/dsh-lanmode.

Unraid template

First run (required): the container runs as UID 1000. Before starting, from the Unraid terminal:

mkdir -p /mnt/user/appdata/deepseek-harness/workspace
chown -R 1000:1000 /mnt/user/appdata/deepseek-harness

Otherwise dsh fails with EACCES on /home/node/.dsh.

<?xml version="1.0"?>
<Container version="2">
  <Name>DeepSeek-Harness</Name>
  <Repository>gitea.milesward.dev/mward4/deepseek-harness:0.1.1-rc.2</Repository>
  <Registry>https://gitea.milesward.dev/mward4/deepseek-harness</Registry>
  <Network>bridge</Network>
  <Shell>sh</Shell>
  <Privileged>false</Privileged>
  <Project>https://github.com/deepseek-ai/deepseek-harness</Project>
  <Support>https://gitea.milesward.dev/mward4/deepseek-harness/issues</Support>
  <Overview>
DeepSeek Harness (dsh) - browser-based agent runtime from DeepSeek AI, packaged from the official npm release (built and served from this Gitea; non-root).

FIRST RUN (required): the container runs as UID 1000. Before starting, from the Unraid terminal:
  mkdir -p /mnt/user/appdata/deepseek-harness/workspace
  chown -R 1000:1000 /mnt/user/appdata/deepseek-harness
Otherwise dsh fails with EACCES on /home/node/.dsh.

SECURITY - READ BEFORE EXPOSING: the dsh Web UI has no authentication or TLS (before 0.1.2), and its web API can execute code on this container. Anything that can reach the port can run commands. Keep it on a trusted LAN, or put an authenticated reverse proxy (e.g. Cloudflare Access, Authelia) in front. Never port-forward it directly to the internet.

POST ARGUMENTS REPLACE THE IMAGE COMMAND ENTIRELY. Always include the patch flag and --no-open, then your trusted hosts:
  web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open --trusted-host YOUR-HOST [--trusted-host YOUR-HOST2]
Without a matching --trusted-host, non-loopback requests get HTTP 403 on /api - that is upstream's DNS-rebinding/CSRF fence working as intended.

The Settings/credentials pages are additionally restricted to loopback pages by upstream design; edit them via SSH tunnel (ssh -L 3080:127.0.0.1:3080 root@unraid-ip, then browse http://127.0.0.1:3080). The @goodandready/dsh-lanmode community plugin can lift that restriction on dsh 0.1.1 (it is NOT compatible with 0.1.2+, and it is third-party code that disables an upstream security fence - review it before trusting it with your credentials).

VERSIONS: this registry publishes immutable version tags only - NO :latest. dsh is a developer preview with breaking changes between releases (0.1.2 adds mandatory launch-token auth: token in docker logs, rotates every restart). Upgrade by editing this tag deliberately. Tags like 0.1.1-rc.2-rN are the same dsh version rebuilt with updated pnpm/base-image security patches.

Your DeepSeek API key can be set below (stored in the container env) or entered later in Settings once running.
  </Overview>
  <Category>AI: Tools:</Category>
  <WebUI>http://[IP]:[PORT:3080]</WebUI>
  <TemplateURL/>
  <Icon>https://gitea.milesward.dev/mward4/deepseek-harness/raw/branch/main/assets/deepseek-logo.png</Icon>
  <ExtraParams/>
  <PostArgs>web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open</PostArgs>
  <Config Name="WebUI Port" Target="3080" Default="3080" Mode="tcp" Description="dsh Web UI. Reachable by everything on your LAN - see the security note in the Overview." Type="Port" Display="always" Required="true" Mask="false">3080</Config>
  <Config Name="DeepSeek API Key" Target="DEEPSEEK_API_KEY" Default="" Description="Optional. Official DeepSeek API key (sk-...). Can also be configured later in the dsh Settings UI." Type="Variable" Display="always" Required="false" Mask="true"></Config>
  <Config Name="DSH Home" Target="/home/node/.dsh" Default="/mnt/user/appdata/deepseek-harness" Mode="rw" Description="Persistent dsh state: settings, credentials, sessions, plugins. Must be owned by UID/GID 1000 (see Overview)." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/deepseek-harness</Config>
  <Config Name="Workspace" Target="/workspace" Default="/mnt/user/appdata/deepseek-harness/workspace" Mode="rw" Description="The agent's working directory. Only mount data you are willing to let the agent read and modify. Must be writable by UID 1000." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/deepseek-harness/workspace</Config>
</Container>

Keeping current

.gitea/workflows/sync-upstream.yml runs every 6 hours on the Gitea Actions runners:

  1. Force-syncs upstream master and dsh-v* tags into this repo.
  2. Builds a new immutable version tag when a new @deepseek-ai/dsh appears on npm and the registry lacks it. Existing tags are never re-pushed, and latest is never published or moved — a new upstream release cannot change what a running deployment executes.
  3. Toolchain freshness check for the pinned default version: if a newer pnpm release or an updated node:22-bookworm-slim base image (which is where npm itself comes from) has shipped since the newest existing build, CI builds a <version>-r<N> revision tag — same dsh, patched toolchain. Adopting it is still a deliberate template edit.

Every build must pass a smoke test before it is pushed: dsh --version and pnpm --version match the intended versions, and the web server boots with the baked patch and answers HTTP on the container's non-loopback interface (200 on pre-token versions; 303/401 on 0.1.2+, which proves the server is up behind its launch-token gate). A dsh release the Cordis patch no longer applies to fails here, loudly, instead of at runtime.

Validating an image

.gitea/workflows/validate-suspect-image.yml (manual dispatch) forensically compares any third-party deepseek-harness image already present on the runner host's docker daemon against this repository's trusted build — without ever executing the suspect image (docker create + filesystem export only). It reports: image config/entrypoint/labels, whether any containers use it, a byte-level diff of dsh's own code against the official release, dependency-tree drift, and tampering red flags (patched-out 0.0.0.0 refusal, startup hooks, unexpected network callouts).

Ran 2026-09-02 against runzhliu/deepseek-harness:0.1.1-rc.2: dsh code byte-identical to the official npm release; no tampering found.