Name colliding plugin layers at boot; note the 0.1.5 file-upload collision
Sync upstream and build images / sync-and-build (push) Successful in 2m18s
Sync upstream and build images / sync-and-build (push) Successful in 2m18s
dsh 0.1.5 ships file upload under the loader id file-upload, which the community dsh-file-upload plugin also registers. A profile that still carries that plugin dies at boot with "duplicate loader entry id: file-upload" and the stack trace never names the plugin. The entrypoint now compares the profile's plugin layers (and the two hand-edited patch layers) against the installed version's own bundle patches before every start and logs the offending plugin with its remove command. Advisory only: the start is never blocked, and only the profile manifest and patch YAML are read as data. The 0.1.5+ release footer carries the caveat, and the publisher now re-renders a release when its text changes rather than only when the digest does. README: upgrade checklist covers this class of failure; the Highlights bullets that still described the pre-mirror tag/release model are corrected.
This commit is contained in:
1 parent
dfaa1a82fc
commit
50121a390f
3 files changed
+90
-14
No files matched your search
@@ -116,6 +116,7 @@ function containerNotes(ver) {
|
||||
}
|
||||
if (atLeast(ver, '0.1.5')) {
|
||||
out.push('Session logs are migrated to format V3 on first start and cannot be read by older versions — back up appdata before moving up to this line.');
|
||||
out.push('File upload is built in from this line (loader id `file-upload`). A profile that still carries the community `dsh-file-upload` plugin does not boot (`duplicate loader entry id: file-upload`) — remove it before updating: `dsh plugin --profile web remove dsh-file-upload`.');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -227,13 +228,20 @@ async function publish(ver) {
|
||||
const existing = await gitea('GET', `/repos/${REPO}/releases/tags/${tag}`);
|
||||
if (existing.status === 200) {
|
||||
const prev = existingDigest(existing.json.body);
|
||||
if (prev === facts.digest && existing.json.name === title) { console.log(`${tag}: up to date`); return; }
|
||||
const history = [prev, ...existingHistory(existing.json.body)].filter((d) => d && d !== facts.digest).slice(0, HISTORY_MAX);
|
||||
const body = renderBody(ver, facts, up, history);
|
||||
// Same image, same title, same rendered text -> nothing to do. Comparing
|
||||
// the text (not just the digest) lets a changed container note or edited
|
||||
// upstream notes reach existing releases on the next run.
|
||||
const norm = (s) => (s || '').replace(/\r\n/g, '\n').trim();
|
||||
const sameImage = prev === facts.digest;
|
||||
if (sameImage && existing.json.name === title && norm(existing.json.body) === norm(body)) { console.log(`${tag}: up to date`); return; }
|
||||
if (DRY_RUN) { console.log(`\n===== DRY RUN update ${tag} =====\n${title}\n\n${body}\n`); return; }
|
||||
const r = await gitea('PATCH', `/repos/${REPO}/releases/${existing.json.id}`, { name: title, body, prerelease, draft: false });
|
||||
if (r.status !== 200) throw new Error(`${tag}: update HTTP ${r.status} ${r.text.slice(0, 200)}`);
|
||||
console.log(`${tag}: updated (image rebuilt: ${prev.slice(0, 19)} -> ${facts.digest.slice(0, 19)})`);
|
||||
console.log(sameImage
|
||||
? `${tag}: updated (notes)`
|
||||
: `${tag}: updated (image rebuilt: ${prev.slice(0, 19)} -> ${facts.digest.slice(0, 19)})`);
|
||||
return;
|
||||
}
|
||||
if (existing.status !== 404) throw new Error(`${tag}: release lookup HTTP ${existing.status}`);
|
||||
|
||||
@@ -115,15 +115,22 @@ Three things went wrong with the alternatives, and this repo is shaped by each:
|
||||
- **Plugin manager works in-container.** pnpm is present on `PATH`, and both `dsh` and
|
||||
the `pnpm` family are wrapped so a root `docker exec` re-execs as the runtime user —
|
||||
plugin installs can't leave root-owned files that break the next start.
|
||||
- **Plugin collisions are named, not guessed at.** When a new dsh version ships built in
|
||||
what a third-party plugin used to add, dsh dies at boot with
|
||||
`duplicate loader entry id: <id>` and no hint which plugin. Before every start the
|
||||
entrypoint compares the profile's plugin layers against the installed version's own
|
||||
bundle patches and logs the offending plugin with its remove command.
|
||||
|
||||
#### Operations
|
||||
- **Upstream mirror.** The `master` branch tracks GitHub's `master` exactly, synced every
|
||||
six hours. (Upstream's own `dsh-v*` tags are deliberately not mirrored — this repo's
|
||||
tag list is exclusively its image releases; each release links to the upstream tag.)
|
||||
- **Releases.** Every image tag gets a matching `v<tag>` [release](../../releases) —
|
||||
digest, toolchain versions, the upstream release's English highlights, and the
|
||||
deployment caveats that apply to that version — generated by CI from the image's own
|
||||
provenance labels.
|
||||
- **Upstream mirror.** The `master` branch and the `dsh-v*` tags track GitHub exactly,
|
||||
synced every six hours; the Tags page here reads the same as upstream's.
|
||||
- **Releases.** One [release](../../releases) per built version, on upstream's tag and
|
||||
titled like upstream — upstream's English notes, then a short container footer (digest,
|
||||
pnpm, base image, build) plus the caveats that apply to that version — generated by CI
|
||||
from the image's own provenance labels and kept current when a version is rebuilt.
|
||||
- **Deployment inspector.** A manual job that reports what the container on the host is
|
||||
actually running — image digest, recent log, and how the profile in the state volume is
|
||||
composed (bundle layers, user patches, plugins) — read-only, credentials never read.
|
||||
- **Forensic validation workflow.** A manual job that diffs any third-party
|
||||
`deepseek-harness` image on the host against the trusted build without executing it.
|
||||
|
||||
@@ -227,7 +234,7 @@ trusts.
|
||||
| Base | `node:22-bookworm-slim` + `git` + `ca-certificates` |
|
||||
| Installed | `@deepseek-ai/dsh@<version>` and `pnpm@<version>` as npm globals (exact versions recorded as image labels) |
|
||||
| Runs as | `PUID:PGID`, default **1000:1000**. The entrypoint starts as root to repair volume ownership, then `setpriv`s down — dsh itself is never root. |
|
||||
| `ENTRYPOINT` | `["docker-entrypoint.sh"]` — fixes ownership, drops privileges, `exec`s `dsh "$@"` |
|
||||
| `ENTRYPOINT` | `["docker-entrypoint.sh"]` — fixes ownership, warns about plugin layers that collide with the installed version, drops privileges, `exec`s `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/config. **Mount this.** Taken over at startup. |
|
||||
@@ -268,10 +275,21 @@ that happens whenever you accept an update Unraid offers. Before you do:
|
||||
changed upstream and what it means for this deployment.
|
||||
2. **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.
|
||||
3. **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*
|
||||
updating:
|
||||
`docker exec <container> dsh plugin --profile web remove @goodandready/dsh-lanmode`
|
||||
3. **Reconcile plugins first.** Upstream keeps absorbing what community plugins did, and a
|
||||
plugin whose loader row id a new version ships built in stops dsh at boot with
|
||||
`duplicate loader entry id: <id>` — the stack trace never names the plugin, so the
|
||||
entrypoint does: it checks the profile against the installed version before every start
|
||||
and logs the plugin and the remove command. Known cases:
|
||||
- `0.1.5+` ships file upload → remove `dsh-file-upload`.
|
||||
- `0.1.2+` → `@goodandready/dsh-lanmode` ≤ 0.6.8 breaks the web client
|
||||
(`web boot: … waiting for services: remote.*`); update it to 0.7 or remove it.
|
||||
|
||||
`docker exec <container> dsh plugin --profile web remove <package>` — or, once the
|
||||
container no longer stays up, the same command against the volume:
|
||||
```sh
|
||||
docker run --rm -v /mnt/user/appdata/deepseek-harness:/home/node/.dsh \
|
||||
gitea.milesward.dev/mward4/deepseek-harness:latest dsh plugin --profile web remove <package>
|
||||
```
|
||||
4. **Apply the update** (or edit the Repository tag, if you pin).
|
||||
|
||||
### What changes in `0.1.2+`: launch-token auth
|
||||
|
||||
@@ -45,6 +45,56 @@ needs_chown() {
|
||||
[ -n "$(find "$1" \( ! -uid "$PUID" -o ! -gid "$PGID" \) -print -quit 2>/dev/null)" ]
|
||||
}
|
||||
|
||||
# A third-party plugin whose bundle layer inserts a loader row id that the
|
||||
# shipped bundles also define kills dsh at boot with `duplicate loader entry
|
||||
# id: <id>` — and nothing in that stack trace names the plugin. It happens
|
||||
# whenever upstream absorbs what a community plugin did (0.1.5 took
|
||||
# `file-upload` from the dsh-file-upload plugin this way). Name the plugin and
|
||||
# the remedy up front. The id list is read from the very patch files dsh
|
||||
# composes, so it tracks whatever version is installed. Advisory only: the
|
||||
# start is never blocked, and only the profile's own manifest and patch YAML
|
||||
# are read (as plain JSON/text — nothing from the volume is executed).
|
||||
DSH_PKG=/usr/local/lib/node_modules/@deepseek-ai/dsh
|
||||
warn_plugin_collisions() {
|
||||
profile="$1"
|
||||
manifest="$STATE/profiles/$profile/package.json"
|
||||
[ -f "$manifest" ] || return 0
|
||||
shipped="$(cat "$DSH_PKG/node_modules/@deepseek-ai/dsh-base/cordis.patch.yml" \
|
||||
"$DSH_PKG/node_modules/@deepseek-ai/dsh-web-app/cordis.patch.yml" 2>/dev/null \
|
||||
| sed -n 's/^[[:space:]]*-[[:space:]]*id:[[:space:]]*//p' | sort -u)"
|
||||
[ -n "$shipped" ] || return 0
|
||||
version="$(node -e 'console.log(require(process.argv[1] + "/package.json").version)' "$DSH_PKG" 2>/dev/null || echo '?')"
|
||||
node -e 'for (const b of require(process.argv[1]).dsh?.profile?.bundles ?? []) if (!b.startsWith("@deepseek-ai/")) console.log(b)' "$manifest" 2>/dev/null \
|
||||
| while IFS= read -r pkg; do
|
||||
dir="$STATE/profiles/$profile/node_modules/$pkg"
|
||||
patch="$(node -e 'console.log(require(process.argv[1] + "/package.json").dsh?.bundle?.patch ?? "")' "$dir" 2>/dev/null || true)"
|
||||
[ -n "$patch" ] && [ -f "$dir/$patch" ] || continue
|
||||
# Only rows nested under an `insert:` list register new ids; a top-level
|
||||
# `- id:` row is a legitimate override of a shipped row, not a collision.
|
||||
inserted="$(sed -n 's/^[[:space:]]\{1,\}-[[:space:]]*id:[[:space:]]*//p' "$dir/$patch" | sort -u)"
|
||||
dup="$(printf '%s\n%s\n' "$shipped" "$inserted" | sort | uniq -d | tr '\n' ' ')"
|
||||
[ -z "$dup" ] || {
|
||||
log "WARNING: plugin $pkg registers loader id(s) ${dup}which dsh $version ships built in."
|
||||
log " dsh will refuse to start (\"duplicate loader entry id\"). Remove the plugin:"
|
||||
log " dsh plugin --profile $profile remove $pkg"
|
||||
log " (run it through 'docker run --rm -v <appdata>:$STATE <image> …' if the container will not stay up)"
|
||||
}
|
||||
done
|
||||
# The two hand-edited layers can insert rows too.
|
||||
for f in "$STATE/profiles/$profile/cordis.patch.yml" "$STATE/cordis.patch.yml"; do
|
||||
[ -f "$f" ] || continue
|
||||
inserted="$(sed -n 's/^[[:space:]]\{1,\}-[[:space:]]*id:[[:space:]]*//p' "$f" | sort -u)"
|
||||
dup="$(printf '%s\n%s\n' "$shipped" "$inserted" | sort | uniq -d | tr '\n' ' ')"
|
||||
[ -z "$dup" ] || {
|
||||
log "WARNING: $f inserts loader id(s) ${dup}which dsh $version ships built in."
|
||||
log " dsh will refuse to start (\"duplicate loader entry id\"). Remove that insert from the file."
|
||||
}
|
||||
done
|
||||
}
|
||||
if [ "$1" = web ]; then
|
||||
warn_plugin_collisions web
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" = 0 ]; then
|
||||
# Keep the `node` account aligned with the requested ids so $HOME, group
|
||||
# lookups and setpriv all resolve even for ids with no passwd entry.
|
||||
|
||||
Reference in new issue
Block a user