From 5121d84b536ffb24df17ebc05fea9ab7503347a4 Mon Sep 17 00:00:00 2001 From: Miles Ward Date: Thu, 10 Sep 2026 15:34:47 -0400 Subject: [PATCH] Transparent brand logo, per-tag releases, README rewrite, MIT license - assets/: replace the white-background avatar with a transparent 512px PNG (brand blue #4d6bfe, tight-cropped) plus a 128px header variant. Same path, so the Unraid template icon keeps working. - CI: every image tag in the registry now gets a v git tag on main and a Gitea release carrying digest, dsh/pnpm/base versions, deploy notes and the 0.1.2+ launch-token caveat. Idempotent, so it backfills the three existing tags on the next run. - README rewritten to match OpenPXE/Discord-Authenticator: centered brand header, badges, contents, why/highlights, quick start, Unraid template, image contract, versions & upgrade procedure (0.1.5 session-format V3 has no downgrade), plugins, security model, pipeline, layout. - LICENSE: MIT for the build tooling. Co-Authored-By: Claude Fable 5 --- .gitea/workflows/sync-upstream.yml | 92 +++++++ LICENSE | 21 ++ README.md | 404 +++++++++++++++++++---------- assets/deepseek-logo-128.png | Bin 0 -> 9204 bytes assets/deepseek-logo.png | Bin 13752 -> 48415 bytes 5 files changed, 384 insertions(+), 133 deletions(-) create mode 100644 LICENSE create mode 100644 assets/deepseek-logo-128.png diff --git a/.gitea/workflows/sync-upstream.yml b/.gitea/workflows/sync-upstream.yml index e023087d12..7af5930200 100644 --- a/.gitea/workflows/sync-upstream.yml +++ b/.gitea/workflows/sync-upstream.yml @@ -164,3 +164,95 @@ jobs: docker push "$REF" done + + # Every image tag in the registry gets a matching v git tag + Gitea + # release on main (the Dockerfile state that built it). Idempotent: only + # tags without a release are published, so this also backfills. + - name: Publish releases for image tags without one + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + REG_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.repository }} + RUN_NUMBER: ${{ github.run_number }} + run: | + set -eu + API="https://${REGISTRY}/api/v1" + MAIN_SHA=$(git rev-parse HEAD) + json_escape() { sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | awk '{printf "%s\\n", $0}'; } + + TAGS=$(curl -s -H "Authorization: token ${REG_TOKEN}" \ + "$API/packages/${OWNER}?type=container&q=deepseek-harness&limit=100" \ + | tr ',' '\n' | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' | grep -v '^sha256:' | sort -V) + + for TAG in $TAGS; do + REL="v${TAG}" + CODE=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token ${TOKEN}" "$API/repos/${REPO}/releases/tags/${REL}") + [ "$CODE" = 404 ] || { echo "$REL: release exists"; continue; } + echo "$REL: publishing" + + DIGEST=$(curl -sI -u "${OWNER}:${REG_TOKEN}" \ + -H 'Accept: application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json' \ + "https://${REGISTRY}/v2/${IMAGE}/manifests/${TAG}" | grep -i '^docker-content-digest' | awk '{print $2}' | tr -d '\r') + MAN=$(curl -s -u "${OWNER}:${REG_TOKEN}" \ + -H 'Accept: application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json' \ + "https://${REGISTRY}/v2/${IMAGE}/manifests/${TAG}") + CFG=$(printf '%s' "$MAN" | grep -o '"digest": *"sha256:[a-f0-9]*"' | head -1 | grep -o 'sha256:[a-f0-9]*' || true) + BLOB=$([ -n "$CFG" ] && curl -s -u "${OWNER}:${REG_TOKEN}" "https://${REGISTRY}/v2/${IMAGE}/blobs/$CFG" || echo '{}') + PNPMV=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.pnpm-version":"[^"]*"' | cut -d'"' -f4 || true) + BASEV=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.base-digest":"[^"]*"' | cut -d'"' -f4 || true) + VER=$(printf '%s' "$TAG" | sed 's/-r[0-9]*$//') + PRE=false; case "$VER" in *alpha*) PRE=true;; esac + case "$TAG" in + *-r[0-9]*) KIND="Toolchain refresh of dsh ${VER}: same upstream release, rebuilt with the current pnpm and a freshly patched \`node:22-bookworm-slim\` base." + TITLE="${REL} — dsh ${VER} toolchain refresh (pnpm ${PNPMV:-?})";; + *) KIND="Container build of **DeepSeek Harness (dsh) ${VER}** from the official npm release, built and smoke-tested by CI." + TITLE="${REL} — dsh ${VER} · pnpm ${PNPMV:-?} · node 22";; + esac + case "$VER" in + 0.1.0*|0.1.1*) TOKEN_NOTE="";; + *) TOKEN_NOTE="> **Launch-token auth (dsh 0.1.2+):** the Web UI requires a one-time token printed in \`docker logs\`; it rotates on every restart and is exchanged for a ~30-day cookie bound to the origin you open it on. \`@goodandready/dsh-lanmode\` ≤0.6.8 is incompatible with this line.";; + esac + + BODY=$(cat < [--trusted-host ]\` +- First run: \`chown -R 1000:1000 /mnt/user/appdata/deepseek-harness\` + +${TOKEN_NOTE} + +Image tags are immutable — this release will never be rebuilt or moved. See the [README](https://gitea.milesward.dev/mward4/deepseek-harness#versions-and-upgrades) for the upgrade procedure. +EOF +) + BODY_JSON=$(printf '%s' "$BODY" | json_escape) + + # Tag main at the commit that built the image (ignore "already exists"). + curl -s -o /dev/null -w "tag ${REL}: HTTP %{http_code}\n" -X POST \ + -H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \ + "$API/repos/${REPO}/tags" \ + -d "{\"tag_name\":\"${REL}\",\"target\":\"${MAIN_SHA}\",\"message\":\"deepseek-harness image ${TAG}\"}" + curl -s -o /tmp/rel.json -w "release ${REL}: HTTP %{http_code}\n" -X POST \ + -H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \ + "$API/repos/${REPO}/releases" \ + -d "{\"tag_name\":\"${REL}\",\"target_commitish\":\"${MAIN_SHA}\",\"name\":\"${TITLE}\",\"body\":\"${BODY_JSON}\",\"draft\":false,\"prerelease\":${PRE}}" + grep -q '"id"' /tmp/rel.json || { echo "::error::release creation failed for ${REL}"; cat /tmp/rel.json; exit 1; } + done diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..1d3dcc58b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Miles Ward + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index fe614eb7da..f34b648d62 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,152 @@ -DeepSeek logo +

+ DeepSeek Harness +

-# deepseek-harness — trusted mirror & container build +

DeepSeek Harness — trusted container build

-Mirror of the official [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) -with a locally-built, auditable container image for homelab use (Unraid). +

+ Auditable, immutable container images of DeepSeek Harness (dsh), built from the official npm release and served from this Gitea. +

-## Branch layout +

+ Upstream ships no container image. Community images ship someone else's build.
+ This repo turns the official release into an image whose every byte you can trace — and keeps it that way on a schedule. +

-| 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. | +

+ pinned + newest + tags + ci + base + license +

-## Image +--- -``` -gitea.milesward.dev/mward4/deepseek-harness: e.g. 0.1.1-rc.2 -gitea.milesward.dev/mward4/deepseek-harness:-r toolchain refresh of the same version +`dsh` is DeepSeek's open-source agent harness: a browser UI that drives an AI agent +with shell access over a workspace. It is distributed only as the npm package +`@deepseek-ai/dsh`, so anyone who wants it in a container either trusts a third-party +image or builds their own. This repository is the "build your own" option, done +properly: the upstream source is mirrored here, the image installs the exact pinned +official release on a slim Node base, and CI re-verifies and republishes on a cadence — +without ever changing what a running deployment executes. + +> **Status.** The deployed version is pinned to **`0.1.1-rc.2`**. Newer upstream +> releases are built and published automatically as their own tags (currently up to +> `0.1.5-rc.1`) but nothing moves until the template is edited by hand — see +> [Versions and upgrades](#versions-and-upgrades) for why, and for what changed in +> `0.1.2+` before you jump. + +## Contents + +- [Why this image](#why-this-image) +- [Highlights](#highlights) +- [Quick start](#quick-start) +- [Unraid](#unraid) +- [Image contract](#image-contract) +- [Versions and upgrades](#versions-and-upgrades) +- [Plugins](#plugins) +- [Security model](#security-model) +- [How it stays current](#how-it-stays-current) +- [Validating a third-party image](#validating-a-third-party-image) +- [Repository layout](#repository-layout) +- [License](#license) + +## Why this image + +Three things went wrong with the alternatives, and this repo is shaped by each: + +- **A community image is an unverifiable build.** The obvious candidate, + `runzhliu/deepseek-harness`, turned out to be honest — this repo's forensic workflow + found its `dsh` code byte-identical to the official release — but that took a + filesystem-level diff to establish, and a 1.6 GB image bundling a Chromium desktop + was never going to be re-audited on every update. Here the provenance is a chain you + can follow: **upstream tag → upstream npm release → this Dockerfile → this registry**, + with the Dockerfile in plain sight and no upstream code modified. +- **`:latest` silently upgraded a live deployment.** A routine container restart pulled + a moving tag and jumped from `0.1.1-rc.2` to the `0.1.2` line, which added mandatory + launch-token auth and swapped the API gateway — breaking remote access and a plugin in + one go. So this registry publishes **immutable version tags only**. There is no + `:latest`, and CI is incapable of re-pushing an existing tag. +- **Upstream refuses to bind `0.0.0.0` from the CLI**, on purpose (the web UI can execute + code). The image doesn't patch that out; it uses upstream's own supported opt-in — a + `--patch` launcher layer — so the safety intent survives and exposure is decided + entirely by which ports you publish. + +## Highlights + +#### Provenance & integrity +- **Official release, pinned exactly.** `npm install -g @deepseek-ai/dsh@` from + `registry.npmjs.org`, nothing else. The version is a build argument with a known-good + default. +- **Immutable tags.** `` for each upstream release, `-r` for a + toolchain refresh of the same release. A tag's digest never changes. +- **Smoke-tested before push.** Every build must report the intended `dsh` and `pnpm` + versions *and* boot the web server with the baked patch and answer HTTP on its + non-loopback interface — so a release the patch no longer applies to fails in CI, + loudly, not on your NAS. +- **Toolchain freshness.** CI compares the newest build of the pinned version against the + current pnpm release and the current `node:22-bookworm-slim` digest (where `npm` + itself comes from); if either moved, it publishes a `-r` revision with the patched + toolchain. + +#### Runtime hygiene +- **Non-root, volume-confined.** Runs as `node` (UID/GID 1000). Everything writable — dsh + state, plugin installs, pnpm's store and cache — lives inside the `/home/node/.dsh` + volume. Verified: a full plugin install writes zero bytes to the rootfs. +- **Headless by contract.** `--no-open` is baked into `CMD`; a server never launches a + browser (on `0.1.2+` an in-container browser could burn the one-time launch token). +- **Plugin manager works in-container.** pnpm is present on `PATH`, so + `dsh plugin --profile web add|update|remove` runs via `docker exec`. + +#### Operations +- **Upstream mirror.** The `master` branch and every `dsh-v*` tag track GitHub exactly, + synced every six hours. +- **Releases.** Every image tag gets a matching `v` [release](../../releases) with + its digest, toolchain versions, and deployment notes — generated by CI. +- **Forensic validation workflow.** A manual job that diffs any third-party + `deepseek-harness` image on the host against the trusted build without executing it. + +## Quick start + +```bash +mkdir -p dsh/state dsh/workspace && sudo chown -R 1000:1000 dsh + +docker run -d --name dsh -p 3080:3080 \ + -v "$PWD/dsh/state:/home/node/.dsh" \ + -v "$PWD/dsh/workspace:/workspace" \ + gitea.milesward.dev/mward4/deepseek-harness:0.1.1-rc.2 \ + web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open --trusted-host $(hostname) + +open http://$(hostname):3080 ``` -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. +Two things in that command are load-bearing. The volumes must be owned by UID 1000 or +`dsh` fails with `EACCES` on first start. And `--trusted-host` must name every hostname +or IP you will open the UI at (port-less entries match any port) — without it, anything +but a loopback `Host` header gets **HTTP 403 on `/api`**. That's upstream's +DNS-rebinding/CSRF fence doing its job, not a broken container. -**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. +The image is `linux/amd64`, built on the Unraid host that runs it. -**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. +## Unraid -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 [--trusted-host ] - ``` - -- `--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: - -```sh -docker exec dsh plugin --profile web add -docker exec dsh plugin --profile web update -docker exec dsh plugin --profile web remove -``` - -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 `. -- 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 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: +**First run (required):** the container runs as UID 1000. From the Unraid terminal: ```sh 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`. +Then add the container from the template below (Docker tab → Add Container → paste, or +drop it in `/boot/config/plugins/dockerMan/templates-user/`). Two fields are the whole +operating contract: + +- **Repository** is an explicit version tag. Never `:latest` — it doesn't exist here. +- **Post Arguments replace the image command entirely.** Always start with + `web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open`, then add one + `--trusted-host` per name you'll browse from. ```xml @@ -144,7 +175,7 @@ Without a matching --trusted-host, non-loopback requests get HTTP 403 on /api - 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. +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; 0.1.5 migrates session logs to a format that cannot be downgraded). Upgrade by editing this tag deliberately, after backing up appdata. 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. @@ -161,40 +192,147 @@ Your DeepSeek API key can be set below (stored in the container env) or entered ``` -## Keeping current +Behind a Cloudflare Tunnel, add the public hostname as a `--trusted-host` and have the +tunnel rewrite `Host` to that name (and strip `Origin`) so the fence sees a name it +trusts. -[`.gitea/workflows/sync-upstream.yml`](.gitea/workflows/sync-upstream.yml) -runs every 6 hours on the Gitea Actions runners: +## Image contract -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 `-r` revision tag — same dsh, patched - toolchain. Adopting it is still a deliberate template edit. +| | | +|---|---| +| Base | `node:22-bookworm-slim` + `git` + `ca-certificates` | +| Installed | `@deepseek-ai/dsh@` and `pnpm@` as npm globals (exact versions recorded as image labels) | +| User | `node` — UID/GID **1000** | +| `ENTRYPOINT` | `["dsh"]` | +| `CMD` | `["web", "--patch", "/opt/deepseek-harness/web.cordis.patch.yml", "--no-open"]` | +| Port | `3080` | +| `/home/node/.dsh` | `DSH_HOME` — settings, credentials, sessions, plugins, pnpm store/cache. **Mount this.** | +| `/workspace` | The agent's working directory (`WORKDIR`). Mount only what the agent may read and modify. | +| `/opt/deepseek-harness/web.cordis.patch.yml` | The baked bind patch: `webserver.host: 0.0.0.0`, `port: 3080` | +| Labels | `dev.milesward.dsh-version`, `dev.milesward.pnpm-version`, `dev.milesward.base-digest` — what CI's freshness check reads | -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. +Two rules about the command line that are easy to get wrong: -## Validating an image +- **`--patch` must come immediately after `web`.** It's a launcher flag; placed after the + web command's own flags it fails with `unknown option '--patch'`. +- **Nothing deployment-specific is baked in.** No `--trusted-host`, no keys, no + hostnames. Those are yours to supply. -[`.gitea/workflows/validate-suspect-image.yml`](.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). +## Versions and upgrades -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. +| Tag | dsh | Role | +|---|---|---| +| `0.1.1-rc.2` | 0.1.1-rc.2 | **Pinned / deployed.** No launch-token auth; `dsh-lanmode` compatible. | +| `0.1.2-rc.1` | 0.1.2-rc.1 | First launch-token release; Remote gateway replaces the ApiProxy. | +| `0.1.5-rc.1` | 0.1.5-rc.1 | Current upstream `latest`. Session format V3 — **no downgrade** once migrated. | + +The full list, with digests and notes, is on the [Releases](../../releases) page — one +release per image tag, published by CI. + +`dsh` is a developer preview and upstream says so: expect breaking changes between +releases. An upgrade here is always three deliberate steps: + +1. **Back up the state volume** (`/mnt/user/appdata/deepseek-harness`). From `0.1.5` + upstream migrates session logs to a new format that cannot be read by older versions. +2. **Reconcile plugins first.** `@goodandready/dsh-lanmode` ≤ 0.6.8 breaks the web client + on `0.1.2+` (`web boot: … waiting for services: remote.*`). Remove or update it *before* + changing the tag: + `docker exec DeepSeek-Harness dsh plugin --profile web remove @goodandready/dsh-lanmode` +3. **Edit the Repository tag** in the template and apply. + +### What changes in `0.1.2+`: launch-token auth + +From `0.1.2` the Web UI is gated by a one-time launch token: + +- The tokened URL is printed at startup — `docker logs DeepSeek-Harness`. +- The token **rotates on every container restart**. +- Opening the URL exchanges it for a ~30-day cookie **bound to the exact origin you used**. + Do the exchange on the hostname you actually intend to browse from (your tunnel + domain, not the LAN IP), or you'll be doing it twice. +- Requests without token or cookie get `dsh web authentication required`. + +## Plugins + +Plugins live in the state volume (`/home/node/.dsh/profiles/web`), never in the image, so +image rebuilds and version bumps don't touch them. Manage them with dsh's own tooling, +which works because pnpm ships in the image: + +```sh +docker exec DeepSeek-Harness dsh plugin --profile web add +docker exec DeepSeek-Harness dsh plugin --profile web update +docker exec DeepSeek-Harness dsh plugin --profile web remove +``` + +On the third-party plugin question: `@goodandready/dsh-lanmode` exists because upstream +restricts the Settings/credentials pages to loopback origins, and it removes that +restriction. A quick source audit found nothing hostile, but it is unaffiliated code +whose purpose is disabling a security fence in front of your API keys. The +fence-preserving alternative is an SSH tunnel: +`ssh -L 3080:127.0.0.1:3080 root@` then `http://127.0.0.1:3080`. + +## Security model + +Be clear about what this thing is: **a web page that runs shell commands on the +container as the agent**. Everything below follows from that. + +- **Reachability is the perimeter.** Before `0.1.2` there is no authentication; from + `0.1.2` there is a launch token, but no TLS. Keep the port on a trusted LAN or behind an + authenticated proxy (Cloudflare Access, Authelia). Never port-forward it to the internet. +- **The trust fence is a feature.** `/api` refuses any `Host` that isn't loopback or + listed in `--trusted-host` — that is upstream's DNS-rebinding and CSRF protection. + Configure it; don't work around it. +- **Mount deliberately.** `/workspace` is what the agent can read and modify. The state + volume holds credentials — back it up and keep it out of anything world-readable. +- **The container is unprivileged.** Non-root, no capabilities, nothing writable outside + the mounted volume. Upstream's own [safety notice](https://github.com/deepseek-ai/deepseek-harness/blob/master/SAFETY.md) + still applies: dsh has not been security-audited, and its sandboxing does not guarantee + isolation. + +## How it stays current + +[`sync-upstream.yml`](.gitea/workflows/sync-upstream.yml) runs on the Gitea Actions +runners every six hours (`17 */6 * * *`) and on any change to the build inputs: + +1. **Mirror** — force-syncs upstream `master` and every `dsh-v*` tag into this repo. +2. **New releases** — if npm's `latest` for `@deepseek-ai/dsh` has no tag in the registry, + builds it, smoke-tests it, pushes it. +3. **Freshness** — reads the toolchain labels off the newest build of the pinned version + and compares them to the current pnpm release and `node:22-bookworm-slim` digest. If + either moved, publishes `-r` with the updated toolchain. +4. **Releases** — every image tag without a `v` release gets one, pointing at the + `main` commit that built it, with digest, toolchain, and deployment notes. + +Nothing in this pipeline can change an existing tag or publish `latest`. Adopting any +new build — release or revision — is a template edit. + +## Validating a third-party image + +[`validate-suspect-image.yml`](.gitea/workflows/validate-suspect-image.yml) is a manual +job for the question this repo started with: *is that image someone handed me actually +the official code?* Given an image already present on the runner host's daemon, it +`docker create`s (never runs) both it and the trusted build, exports both filesystems, +and reports: 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 red flags — +patched-out `0.0.0.0` refusal, startup hooks, unexpected network callouts. + +Run 2026-09-02 against `runzhliu/deepseek-harness:0.1.1-rc.2`: `dsh` byte-identical to +the official release, upstream safety check intact, no tampering. + +## Repository layout + +| Branch / path | Contents | +|---|---| +| `master` | Exact mirror of upstream `master` plus all `dsh-v*` tags. Never edited here. | +| `main` | This branch — the build. | +| `Dockerfile` | The image. `ARG DSH_VERSION` is the pinned default. | +| `web.cordis.patch.yml` | The `0.0.0.0` bind patch, applied via `--patch`. | +| `.gitea/workflows/` | `sync-upstream.yml` (mirror · build · freshness · releases), `validate-suspect-image.yml` (forensics). | +| `assets/` | Logo (transparent PNG, also the Unraid template icon). | + +Release tags on `main` are `v`; upstream's mirrored tags are `dsh-v`. + +## License + +The build tooling in this repository is **MIT**. DeepSeek Harness itself is MIT-licensed +by DeepSeek AI; the DeepSeek name and logo belong to them and are used here to identify +what the image contains. diff --git a/assets/deepseek-logo-128.png b/assets/deepseek-logo-128.png new file mode 100644 index 0000000000000000000000000000000000000000..b1019af3f295100b296e5b4c7c2ec3a90a8fabf1 GIT binary patch literal 9204 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