Sync upstream and build images / sync-and-build (push) Failing after 22s
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]>
95 lines
4.1 KiB
Bash
95 lines
4.1 KiB
Bash
#!/bin/sh
|
|
# Make the container's mounted volumes usable no matter how the host owns them.
|
|
#
|
|
# A bind mount arrives with the HOST's ownership, so an image that simply runs
|
|
# as uid 1000 dies with `EACCES … mkdir '/home/node/.dsh/profiles/web'` on any
|
|
# freshly created appdata directory. The fix has to happen inside the
|
|
# container, and only root can chown. So this entrypoint starts as root, takes
|
|
# ownership of the state volume, and then drops to PUID:PGID (default
|
|
# 1000:1000) with setpriv — dsh itself never runs as root, and there is no
|
|
# setuid binary left behind for it to climb back through.
|
|
#
|
|
# PUID/PGID follow the usual Unraid convention: set them if your appdata is
|
|
# owned by something else (Unraid's share default is 99:100) and the volume is
|
|
# adjusted to match at startup.
|
|
#
|
|
# If the container is started with an explicit user (`--user`), repairing
|
|
# ownership is impossible; this prints one actionable line instead of a Node
|
|
# stack trace.
|
|
set -eu
|
|
|
|
PUID="${PUID:-1000}"
|
|
PGID="${PGID:-1000}"
|
|
STATE="${DSH_HOME:-/home/node/.dsh}"
|
|
HOME_DIR=/home/node
|
|
WORKSPACE=/workspace
|
|
PATCH=/opt/deepseek-harness/web.cordis.patch.yml
|
|
|
|
log() { printf 'dsh-entrypoint: %s\n' "$*" >&2; }
|
|
|
|
# Unraid Post Arguments replace the image CMD wholesale, so a bare flag list
|
|
# ("--trusted-host tower.local") would otherwise reach dsh with no subcommand.
|
|
# Prepend the default web invocation in that case; a full command line
|
|
# ("web --patch … --no-open") is passed through untouched.
|
|
if [ "$#" -eq 0 ]; then
|
|
set -- web --patch "$PATCH" --no-open
|
|
else
|
|
case "$1" in
|
|
-*) set -- web --patch "$PATCH" --no-open "$@" ;;
|
|
esac
|
|
fi
|
|
|
|
# True when anything under $1 is not owned by PUID:PGID. `-print -quit` stops
|
|
# at the first hit, so the common "already correct" case costs one stat.
|
|
needs_chown() {
|
|
[ -n "$(find "$1" \( ! -uid "$PUID" -o ! -gid "$PGID" \) -print -quit 2>/dev/null)" ]
|
|
}
|
|
|
|
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.
|
|
CUR_UID="$(id -u node)"
|
|
CUR_GID="$(id -g node)"
|
|
[ "$PGID" = "$CUR_GID" ] || { log "moving group node to gid $PGID"; groupmod -o -g "$PGID" node; }
|
|
[ "$PUID" = "$CUR_UID" ] || { log "moving user node to uid $PUID"; usermod -o -u "$PUID" node; }
|
|
|
|
mkdir -p "$STATE" "$WORKSPACE"
|
|
chown "$PUID:$PGID" "$HOME_DIR" 2>/dev/null || true
|
|
|
|
# State volume is entirely dsh's own (settings, credentials, sessions,
|
|
# plugins, pnpm store) — safe to take wholesale.
|
|
if needs_chown "$STATE"; then
|
|
log "taking ownership of $STATE for ${PUID}:${PGID}"
|
|
chown -R "$PUID:$PGID" "$STATE"
|
|
fi
|
|
|
|
# The workspace holds the operator's data, so only the mount point itself
|
|
# is adjusted (enough for the agent to create files). Rewriting ownership
|
|
# across someone's project tree is opt-in, never a silent side effect.
|
|
if [ "$(stat -c '%u:%g' "$WORKSPACE")" != "${PUID}:${PGID}" ]; then
|
|
chown "$PUID:$PGID" "$WORKSPACE"
|
|
fi
|
|
if [ "${DSH_CHOWN_WORKSPACE:-0}" = 1 ]; then
|
|
if needs_chown "$WORKSPACE"; then
|
|
log "DSH_CHOWN_WORKSPACE=1: taking ownership of everything under $WORKSPACE"
|
|
chown -R "$PUID:$PGID" "$WORKSPACE"
|
|
fi
|
|
elif needs_chown "$WORKSPACE"; then
|
|
log "note: some files under $WORKSPACE are not owned by ${PUID}:${PGID}; the agent"
|
|
log " may be unable to modify them. Set DSH_CHOWN_WORKSPACE=1 to take ownership."
|
|
fi
|
|
|
|
export HOME="$HOME_DIR"
|
|
exec setpriv --reuid "$PUID" --regid "$PGID" --init-groups dsh "$@"
|
|
fi
|
|
|
|
# Non-root start (explicit --user): verify, don't guess.
|
|
if [ ! -w "$STATE" ]; then
|
|
log "ERROR: $STATE is not writable by uid $(id -u):$(id -g) — it is owned by $(stat -c '%u:%g' "$STATE" 2>/dev/null || echo '?')."
|
|
log "This container was started with an explicit --user, so it cannot repair that itself."
|
|
log "Either drop --user (the entrypoint then takes ownership at startup), or on the host run:"
|
|
log " chown -R $(id -u):$(id -g) <host path mounted at $STATE>"
|
|
exit 1
|
|
fi
|
|
exec dsh "$@"
|