diff --git a/devBenches/devcontainer.test/README.md b/devBenches/devcontainer.test/README.md index 9765d55..e9cc383 100644 --- a/devBenches/devcontainer.test/README.md +++ b/devBenches/devcontainer.test/README.md @@ -17,6 +17,9 @@ Layer 1a adds developer tools on top of Layer 0: - Zsh and oh-my-zsh with plugins - PATH configuration for all dev tools +The Layer 3 user image adds the effective-user Corepack cache exercised by the +unprivileged pnpm checks in this harness. + ## Quick Start ```bash @@ -42,7 +45,7 @@ docker compose down The `test.sh` script validates: - ✅ Python development tools -- ✅ Node.js development tools +- ✅ Node.js development tools and unprivileged pnpm/Corepack operation - ✅ Python package managers (uv) - ✅ Spec-driven tools (`specify`, `openspec`) - ✅ Worktree-mode Speckit bootstrap installation diff --git a/devBenches/devcontainer.test/test.sh b/devBenches/devcontainer.test/test.sh index 233077c..f04950c 100755 --- a/devBenches/devcontainer.test/test.sh +++ b/devBenches/devcontainer.test/test.sh @@ -68,6 +68,9 @@ echo "=== Node.js Development ===" test_tool_output "Node.js" "node --version" test_tool_output "npm" "npm --version" test_tool_output "yarn" "yarn --version" +test_tool_output "pnpm as unprivileged user" "pnpm --version" +test_tool "Corepack cache is user-owned and writable" \ + "runtime_home=\$(getent passwd \"\$(id -u)\" | cut -d: -f6); cache=\"\$runtime_home/.cache/corepack\"; test -d \"\$cache\" && test -w \"\$cache\" && test \"\$(stat -c '%u' \"\$cache\")\" = \"\$(id -u)\"" test_tool "/usr/local/bin in PATH" "echo \$PATH | grep -q '/usr/local/bin'" echo "" diff --git a/user-layer/Dockerfile b/user-layer/Dockerfile index 53917ac..a76e858 100644 --- a/user-layer/Dockerfile +++ b/user-layer/Dockerfile @@ -18,7 +18,7 @@ FROM ${BASE_IMAGE} # Container version labels LABEL layer="3" LABEL layer.name="user-layer" -LABEL layer.version="1.2.0" +LABEL layer.version="1.2.1" LABEL layer.description="User personalization layer" # Build arguments @@ -30,6 +30,15 @@ ARG EXTRA_CHOWN_DIRS="" USER root +# Corepack's shared base-layer cache is root-owned. Route Corepack and its +# package-manager shims through a runtime wrapper that selects a writable cache +# from the effective user's passwd home, including for non-interactive calls. +COPY corepack-user-cache /usr/local/libexec/workbenches-corepack-user-cache +RUN chmod 0755 /usr/local/libexec/workbenches-corepack-user-cache && \ + for command in corepack pnpm pnpx yarn yarnpkg; do \ + ln -sfn /usr/local/libexec/workbenches-corepack-user-cache "/usr/local/bin/$command"; \ + done + # ======================================== # USER CREATION # ======================================== diff --git a/user-layer/corepack-user-cache b/user-layer/corepack-user-cache new file mode 100644 index 0000000..90b90e4 --- /dev/null +++ b/user-layer/corepack-user-cache @@ -0,0 +1,28 @@ +#!/bin/sh +set -eu + +command_name=${0##*/} +case "$command_name" in + corepack|pnpm|pnpx|yarn|yarnpkg) ;; + *) + echo "Unsupported Corepack command: $command_name" >&2 + exit 64 + ;; +esac + +# Respect an explicit caller override. Replace only the shared image default, +# which is root-owned and therefore unusable by Layer 3 runtime users. +case "${COREPACK_HOME:-}" in + ""|/opt/corepack) + runtime_home=$(getent passwd "$(id -u)" | cut -d: -f6) + if [ -z "$runtime_home" ]; then + echo "Could not determine the effective user's home directory" >&2 + exit 1 + fi + COREPACK_HOME="$runtime_home/.cache/corepack" + export COREPACK_HOME + ;; +esac + +mkdir -p "$COREPACK_HOME" +exec "/usr/bin/$command_name" "$@"