Publishing -r2 exposed this: a buildx multi-arch push stores an OCI index at
the tag, and both label readers assumed a single-arch manifest. Consequences
were a release body with 'pnpm ?' and — much worse — CI's freshness check
seeing the pinned image as permanently stale, which would have published a
new -rN revision every six hours forever.
Both readers now follow an index to a platform manifest (preferring amd64,
the deployment's arch) before reading the config blob, and the publisher
warns when an image carries no provenance labels at all. Verified against
both a multi-arch tag (0.1.1-rc.2-r2) and a single-arch one (0.1.5-rc.2):
the pinned build now reads as current, so no redundant rebuild.
Co-Authored-By: Claude Fable 5 <[email protected]>
Verifying the pushed -r1 image found one hole: only dsh was wrapped, so a
root 'docker exec <c> pnpm ...' created PNPM_HOME/XDG_CONFIG_HOME owned by
root inside the state volume — which the unprivileged dsh could not then use
for plugin installs (self-healing on the next restart, since the entrypoint
re-chowns, but broken until then).
The wrapper is now generic (execs the resolved target directly; dsh's bin.js
is +x with a node shebang and pnpm's bin is an ELF, so no node prefix is
needed) and is installed over dsh, pnpm, pnpx, pn and pnx. Renamed
dsh-wrapper.sh -> run-as-runtime-user.sh to match what it does.
Verified: root 'docker exec pnpm --version' + 'pnpm store path' + 'dsh plugin
add' leave zero root-owned files in the volume; 23-case matrix still green.
Ships as 0.1.1-rc.2-r2.
Co-Authored-By: Claude Fable 5 <[email protected]>
A bind mount arrives with the host's ownership, so a freshly created appdata
directory killed dsh with EACCES on mkdir /home/node/.dsh/profiles/web. Only
root inside the container can fix that, so:
- docker-entrypoint.sh starts as root, aligns the node account with PUID/PGID
(default 1000:1000, Unraid convention), takes ownership of the state volume
when needed, then setpriv's down before exec'ing dsh. The network-reachable
process is never root and no setuid binary is left behind. The workspace
mount point is made writable but its CONTENTS are left alone unless
DSH_CHOWN_WORKSPACE=1 — never silently rewrite ownership across user data.
- Dropped USER node (root is required to chown); dsh is wrapped so a root
re-execs as PUID:PGID and cannot leave
root-owned files in the volume. Wrapper resolves the real bin.js at build
time and self-tests.
- Explicit --user is honored: ownership cannot be repaired, so the entrypoint
verifies writability and exits with one actionable line instead of a Node
stack trace.
- XDG_CONFIG_HOME pinned into the volume too; pnpm was reaching for
/root/.config when HOME was unset.
- CI: labels now carry a recipe hash, and the freshness check cuts a -rN
revision when the recipe changes — so an image fix reaches the pinned
deployment without waiting for an upstream release.
- README/template: chown step removed, PUID/PGID/DSH_CHOWN_WORKSPACE
documented, template pinned to 0.1.1-rc.2-r1.
Verified with a 23-case matrix: root-owned volume self-heals and serves 200,
already-correct volume skips the chown, 99:100 + PUID/PGID works, plugin
add/remove via root docker exec leaves no root-owned files, explicit --user
still works and fails clearly when it can't, both argument forms serve.
Co-Authored-By: Claude Fable 5 <[email protected]>
The generator now receives the Dockerfile's default DSH_VERSION and calls out
that release as the pinned deployment version, so the Releases page says
which tag the Unraid template runs without hand-editing.
Co-Authored-By: Claude Fable 5 <[email protected]>
- CI plan: candidates are npm latest AND next (release candidates), plus an
optional workflow_dispatch 'version' input for any exact version. This is
what was holding back 0.1.5-rc.2 (published as next, not latest).
- Upstream dsh-v* tags are no longer mirrored into this repo (deleted the
existing ones); master stays an exact branch mirror and every release links
to its upstream tag. This repo's tags are now exclusively v<image tag>.
- Dockerfile: build-provenance labels (org.opencontainers.image.revision /
.created, dev.milesward.ci-run) so releases cite the commit and CI run that
actually built the image instead of whatever HEAD is at publish time.
- Releases: shell template replaced by .gitea/scripts/publish-releases.js,
run on the node:22 base image (no runner deps). It pulls the upstream
GitHub release's English notes, adds version-dependent caveats (launch
token from 0.1.2, Session format V3 from 0.1.5), image facts and Unraid
deploy steps. Existing v* tags re-pointed to 2d033c3 (the commit that
built those images); their releases are regenerated in this format.
- README: dynamic badges (shields Gitea release, Gitea workflow status),
0.1.5-rc.2 row, pipeline/tag-namespace docs updated.
Co-Authored-By: Claude Fable 5 <[email protected]>
- 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<tag> 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 <[email protected]>
- 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]>
- Ship /opt/deepseek-harness/web.cordis.patch.yml and pass it via upstream's
--patch launcher flag (must precede the web command's own flags) instead of
seeding DSH_HOME; entrypoint accepts template Post Arguments verbatim.
- Add /workspace (agent WORKDIR) and pin DSH_HOME=/home/node/.dsh; drop the
implicit /home/node volume so templates mount .dsh and /workspace cleanly.
- README: Unraid template (repo/support/icon/versions corrected for this
image, lanmode plugin caution), first-run chown note, DeepSeek logo.
Verified: no-args, flags-only, and full template invocations all serve the
web UI; trust fence still 403s unknown Hosts; state persists to mounted .dsh.
Co-Authored-By: Claude Fable 5 <[email protected]>
The Actions GITHUB_TOKEN can push git refs but the container registry
rejects it (run 1 failure). Also adds a manual, never-executes-the-image
forensic workflow comparing a third-party image on the runner host's
daemon against the trusted build.
Co-Authored-By: Claude Fable 5 <[email protected]>
dsh's webserver schema only accepts 127.0.0.1 or 0.0.0.0; the CLI refuses
0.0.0.0 by design. Seed $DSH_HOME/profiles/web/cordis.patch.yml (the
supported override) from the entrypoint instead. Verified: UI serves 200
through a published port, /api trust fence 403s unknown Hosts, and
--trusted-host pass-through lifts it.
Co-Authored-By: Claude Fable 5 <[email protected]>