# ============================================================================== # HolyClaude — Pre-configured Docker Environment for Claude Code CLI + CloudCLI # https://github.com/coderluii/holyclaude # # Build variants: # docker build -t holyclaude . # full (default) # docker build --build-arg VARIANT=slim -t holyclaude:slim . # ============================================================================== ARG VARIANT=full FROM golang:1.27.1-bookworm@sha256:69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195 AS esbuild-builder ARG TARGETARCH RUN case "$TARGETARCH" in amd64) ;; arm64) ;; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac; \ set -eux; \ for ESBUILD_VERSION in 0.15.18 0.18.20 0.25.12; do \ mkdir -p "/out/${ESBUILD_VERSION}"; \ CGO_ENABLED=0 GOOS=linux GOARCH="$TARGETARCH" GOBIN="/out/${ESBUILD_VERSION}" \ go install "github.com/evanw/esbuild/cmd/esbuild@v${ESBUILD_VERSION}"; \ test "$("/out/${ESBUILD_VERSION}/esbuild" --version)" = "$ESBUILD_VERSION"; \ done FROM node:26.9.0-bookworm-slim@sha256:c8fedd782bcd1b68d8a7d1ed2577b5f820eba820871323f605292651ff11e3c6 AS ffmpeg-security-builder ENV DEBIAN_FRONTEND=noninteractive ARG TARGETARCH ARG VARIANT COPY --chmod=0755 scripts/build-ffmpeg-security-backport.sh /usr/local/bin/build-ffmpeg-security-backport.sh COPY security/patches/ffmpeg/ /security/patches/ffmpeg/ RUN test -x /usr/local/bin/build-ffmpeg-security-backport.sh && \ mkdir -p /out/ffmpeg-security-backport && \ if [ "$VARIANT" = "full" ]; then \ printf '%s\n' \ 'Types: deb-src' \ 'URIs: http://deb.debian.org/debian' \ 'Suites: bookworm bookworm-updates' \ 'Components: main' \ '' \ 'Types: deb-src' \ 'URIs: http://deb.debian.org/debian-security' \ 'Suites: bookworm-security' \ 'Components: main' \ > /etc/apt/sources.list.d/debian-source.sources && \ apt-get update && \ apt-get install -y --no-install-recommends ca-certificates curl xz-utils patch dpkg-dev build-essential && \ apt-get build-dep -y --no-install-recommends ffmpeg && \ TARGETARCH="$TARGETARCH" /usr/local/bin/build-ffmpeg-security-backport.sh; \ fi FROM python:3.14.9-slim-bookworm@sha256:7e0522950230cad1ec100ada6e3898c22c6bc82851289b4bdf0bf77c1c829cd5 AS python-runtime FROM node:26.12.1-bookworm-slim@sha256:28147155d47dc31abe0a46e0d12b59b2fb3d0a2dcc5dc65fce76a074180b16fe COPY --from=python-runtime /usr/local/ /usr/local/ RUN test "$(python3 --version)" = "Python 3.14.9" && \ python3 -m pip --version >/dev/null ARG HOLYCLAUDE_VERSION=1.6.8 LABEL org.opencontainers.image.source=https://github.com/CoderLuii/HolyClaude LABEL org.opencontainers.image.version=${HOLYCLAUDE_VERSION} # ---------- Build args ---------- ARG S6_OVERLAY_VERSION=3.2.3.2 ARG S6_NOARCH_SHA256=5379750ed30a84bbd2e2dd74847ba6b5bd29cd0b2e3ea2ec58049b57eb2eda12 ARG S6_ARCHIVE_SHA256_AMD64=e6befcc96a437a3831386ecfc51808c5d3e939dc5fe3c02ae9284599e8aa2408 ARG S6_ARCHIVE_SHA256_ARM64=b17f17a82e7a515c682a91edaf2ffdabb73f891981b6c1fd712115693a2f8b4c ARG FZF_VERSION=0.74.5 ARG FZF_ARCHIVE_SHA256_AMD64=d47d7f882972c7d674dbbba998ffa4116e68379616ffac7695d0671e9a722c5f ARG FZF_ARCHIVE_SHA256_ARM64=cf058001da21724a469b714a29cf4aab9cbe860d218a0d6c1b5b661efad7784c ARG CHROMIUM_DEBIAN_VERSION_AMD64=156.0.8078.47-1~deb12u1 ARG CHROMIUM_DEBIAN_VERSION_ARM64=156.0.8078.47-1~deb12u1 ARG CHROMIUM_PACKAGE_SHA256_AMD64=aad2dc4f0d7340343c3dfb9dc9d1b5d058e4b008b6be18ca136415c3fa49ccf2 ARG CHROMIUM_PACKAGE_SHA256_ARM64=a7ade1ba5b63258b899dd469f56edf5b03e0d63260e1bd27c69c2bbc661200e1 ARG CHROMIUM_COMMON_PACKAGE_SHA256_AMD64=e570a2b3da2f4dfdc9edf966b4d9588c67be6021fd09198dbbed3d11c8870f7d ARG CHROMIUM_COMMON_PACKAGE_SHA256_ARM64=e3e3a575718696dc0b40c53dc1f27b946d6c4b5818b8699624af11a2be47b2e3 ARG CHROMIUM_SANDBOX_PACKAGE_SHA256_AMD64=adbb12a68bb15d27eff08438d7372e17407163891a7baad4b95a1c25fd70ac8b ARG CHROMIUM_SANDBOX_PACKAGE_SHA256_ARM64=b6cb89d36042208b929ce7dc8d706c0471c1d65dff844f312d24ae288439bc22 ARG CLAUDE_CODE_VERSION=2.1.307 ARG CLAUDE_INSTALLER_SHA256=3a68d3406cf674e17bed1733a4dcf37805e2e47d87417700007d7e1aa766a944 ARG CLAUDE_BINARY_SHA256_AMD64=6021eb72d20d0682b720bdc63acd2a5cd2270895d1f10d9ca2a88cbfa7b9dd0e ARG CLAUDE_BINARY_SHA256_ARM64=1c67968ab8410c9644e2f4867167a0dab1a8c6c9de9cc83fe78449e5ae8a3633 ARG JUNIE_VERSION=3888.1 ARG JUNIE_ARCHIVE_SHA256_AMD64=742d99abb4a8a950b4a64d397b8763e25133d0708237b706ba5ab2c890745d9c ARG JUNIE_ARCHIVE_SHA256_ARM64=365b966a4de7a82b325e7b2a348eed4aea62b251b130ce2d4118a266979547a3 ARG CURSOR_BUILD_ID=2026.09.15-d2fe57e ARG CURSOR_ARCHIVE_SHA256_AMD64=4b7b026dd104e935b216cc52f905a560d741fc80a4a4d62ef655735b96a15c97 ARG CURSOR_ARCHIVE_SHA256_ARM64=2d741c12c3ee7a505584579efb28a0ee31ff13fefc1f347e2d3b43688c04620d ARG CURSOR_LAUNCHER_SHA256=2ccc9a8e167797641448b5e5c936f006ba137a2555f117f38c5eb76a5238a233 ARG CURSOR_NODE_SHA256_AMD64=e0e46d3a1c0667117303412647cafcbcefb1be7612493015ec8fd6b7440162a4 ARG CURSOR_NODE_SHA256_ARM64=47befb5f57df96771ce343d6293349ecf4d46c91110b626423ec3a49d2fee7c1 ARG SETUPTOOLS_VERSION=84.2.1 ARG SETUPTOOLS_WHEEL_SHA256=ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2 ARG PISCINA_VERSION=4.9.4 ARG PISCINA_ARCHIVE_SHA256=9ea03459396e46c626b8e1b8d73e827ecfbe48ac1e94fb9ba7235e6b25f9fc2f ARG WS_VERSION=8.21.3 ARG WS_ARCHIVE_SHA256=df3454ef205791ce50b5b9241762dcf9bfe1aa9f7f01d3057229be7dac0c2dc3 ARG CLOUDCLI_NANOID_VERSION=3.3.19 ARG CLOUDCLI_NANOID_ARCHIVE_SHA256=4e371b71e3d5081fa0052356d5c1904e7a60e049864c26f0724cfd32dc303849 ARG NESTED_IP_ADDRESS_VERSION=10.7.2 ARG NESTED_IP_ADDRESS_ARCHIVE_SHA256=4301746e43e8a85a6a41e268f02178b27e6ba58e78e6913ab105d3871618083b ARG CLOUDCLI_FAST_URI_VERSION=3.1.8 ARG CLOUDCLI_FAST_URI_ARCHIVE_SHA256=86be033b406a7737c0521edc8fe3e15c7ac0cb6b5e509478cc9539a2efaa086c ARG CLOUDCLI_JS_YAML_VERSION=3.15.2 ARG CLOUDCLI_JS_YAML_ARCHIVE_SHA256=7f005cf0b8ee639b4557e0e321dc067c1f2aa0a442d096e03f2ab53353738794 ARG CLOUDCLI_VSCODE_RIPGREP_PACKAGE_VERSION=1.17.1 ARG CLOUDCLI_RIPGREP_RELEASE_VERSION=15.0.1 ARG CLOUDCLI_RIPGREP_BINARY_VERSION=15.0.0 ARG CLOUDCLI_RIPGREP_BINARY_REVISION_AMD64=3a612f88b8 ARG CLOUDCLI_RIPGREP_ARCHIVE_SHA256_AMD64=4499958bfd5252df3d9e7504127fd448e4a14fbf2805ef4f14baaa1bcf775188 ARG CLOUDCLI_RIPGREP_ARCHIVE_SHA256_ARM64=dd3738a4b6e8df0fb3bc3edc5af352c4c39e0d97ad118a23e5176bdc5d48ba08 ARG UNDICI_8_VERSION=8.10.2 ARG UNDICI_8_ARCHIVE_SHA256=740638ae32d78d2646a6727950e365fa26b6fa87913fa096e60ed4afeb4634aa ARG FULL_XMLDOM_VERSION=0.9.12 ARG FULL_XMLDOM_ARCHIVE_SHA256=08245e18c248b957b4c6e07f8549ad5f55ae11b7a8abd4c1113a0fd61ddc67ee ARG AZURE_CLI_VERSION=2.91.0-1~bookworm ARG AZURE_CLI_INSTALLER_SHA256=01fada4dafe903fa6edae138d3e3ca2e6e4295d7c8a35e48632bba4aa9dbe9d9 ARG AZURE_CLI_PYJWT_VERSION=2.15.1 ARG AZURE_CLI_PYJWT_WHEEL_SHA256=42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 ARG AZURE_CLI_URLLIB3_VERSION=2.8.0 ARG AZURE_CLI_URLLIB3_WHEEL_SHA256=0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 ARG GITHUB_CLI_VERSION=2.103.0 ARG GITHUB_CLI_PACKAGE_SHA256_AMD64=4a48fa23b9ce9581f3304b822f3e014812f26519b134f89b37db8bc7d74746e1 ARG GITHUB_CLI_PACKAGE_SHA256_ARM64=5e218097cf3e02c4855a07fd4f5719b1523fa799f24eeb70d6b795a9a74fffe2 ARG TARGETARCH ARG VARIANT=full ARG FFMPEG_BACKPORT_VERSION=7:5.1.9-0+deb12u1+holyclaude2 # ---------- Environment ---------- ENV DEBIAN_FRONTEND=noninteractive \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ DISPLAY=:99 \ DBUS_SESSION_BUS_ADDRESS=disabled: \ CHROMIUM_FLAGS="--no-sandbox --disable-gpu --disable-dev-shm-usage" \ CHROME_PATH=/usr/bin/chromium \ PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \ NODE_PATH=/usr/local/lib/node_modules # ---------- s6-overlay v3 (multi-arch) ---------- RUN apt-get update && apt-get install -y --no-install-recommends xz-utils curl ca-certificates && rm -rf /var/lib/apt/lists/* RUN S6_ARCH=$(case "$TARGETARCH" in amd64) echo "x86_64";; arm64) echo "aarch64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ S6_ARCH_SHA256=$(case "$TARGETARCH" in amd64) echo "$S6_ARCHIVE_SHA256_AMD64";; arm64) echo "$S6_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ for S6_ASSET in noarch "$S6_ARCH"; do \ S6_EXPECTED_SHA256=$(case "$S6_ASSET" in noarch) echo "$S6_NOARCH_SHA256";; *) echo "$S6_ARCH_SHA256";; esac); \ if ! { \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "/tmp/s6-overlay-${S6_ASSET}.tar.xz" \ "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ASSET}.tar.xz" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "/tmp/s6-overlay-${S6_ASSET}.tar.xz.sha256" \ "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ASSET}.tar.xz.sha256" && \ test "$(cut -d' ' -f1 "/tmp/s6-overlay-${S6_ASSET}.tar.xz.sha256")" = "$S6_EXPECTED_SHA256" && \ echo "$S6_EXPECTED_SHA256 /tmp/s6-overlay-${S6_ASSET}.tar.xz" | sha256sum -c -; \ }; then \ rm -f "/tmp/s6-overlay-${S6_ASSET}.tar.xz" "/tmp/s6-overlay-${S6_ASSET}.tar.xz.sha256"; \ exit 1; \ fi; \ done && \ tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \ tar -C / -Jxpf "/tmp/s6-overlay-${S6_ARCH}.tar.xz" && \ rm -f /tmp/s6-overlay-noarch.tar.xz /tmp/s6-overlay-noarch.tar.xz.sha256 "/tmp/s6-overlay-${S6_ARCH}.tar.xz" "/tmp/s6-overlay-${S6_ARCH}.tar.xz.sha256" # ---------- System packages (always installed) ---------- RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \ # Core utilities git curl wget jq ripgrep fd-find unzip zip tree tmux bat bubblewrap nano \ # Build tools build-essential pkg-config \ # Fonts fonts-liberation2 fonts-dejavu-core fonts-noto-core fonts-noto-color-emoji fonts-inter \ # Locale support locales \ # Debugging tools strace lsof iproute2 procps htop shellcheck dnsutils \ # Database CLI tools postgresql-client redis-tools sqlite3 default-mysql-client \ # SSH/Mosh remote shell support (disabled by default) openssh-client openssh-server mosh \ # Xvfb for headless Chrome xvfb \ # Image processing imagemagick \ # Sudo sudo \ && rm -rf /var/lib/apt/lists/* ARG YQ_VERSION=4.54.1 ARG YQ_SHA256_AMD64=8e34fc298390875de416e6a4afcb8cabeceb25d9aa8506c1a2f9353cf702ea5f ARG YQ_SHA256_ARM64=189088da0c6429ec5178dfaab1a114805f6cab0b61b165ab236efedf1d57a71b ARG ATUIN_VERSION=18.24.1 ARG ATUIN_SHA256_AMD64=fe03f652d6545a9d8795b3c2abb84bcddfe831e4b7effb0c2d191a0d70e498a0 ARG ATUIN_SHA256_ARM64=a63e649138cddd5c9dba7123c89c2488b1701027d432836553a8a6cf51f5e863 RUN set -eux; \ case "$TARGETARCH" in \ amd64) YQ_SHA256="$YQ_SHA256_AMD64"; ATUIN_SHA256="$ATUIN_SHA256_AMD64"; ATUIN_TARGET=x86_64-unknown-linux-musl ;; \ arm64) YQ_SHA256="$YQ_SHA256_ARM64"; ATUIN_SHA256="$ATUIN_SHA256_ARM64"; ATUIN_TARGET=aarch64-unknown-linux-musl ;; \ *) exit 1 ;; \ esac; \ curl --disable --retry 8 --retry-all-errors --connect-timeout 15 --max-time 300 -fsSL \ "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_${TARGETARCH}" -o /usr/local/bin/yq; \ echo "$YQ_SHA256 /usr/local/bin/yq" | sha256sum -c -; \ chmod 0755 /usr/local/bin/yq; \ yq --version | grep -F "$YQ_VERSION"; \ curl --disable --retry 8 --retry-all-errors --connect-timeout 15 --max-time 300 -fsSL \ "https://github.com/atuinsh/atuin/releases/download/v${ATUIN_VERSION}/atuin-${ATUIN_TARGET}.tar.gz" -o /tmp/atuin.tar.gz; \ echo "$ATUIN_SHA256 /tmp/atuin.tar.gz" | sha256sum -c -; \ mkdir /tmp/atuin-release; \ tar -xzf /tmp/atuin.tar.gz -C /tmp/atuin-release; \ install -m 0755 "/tmp/atuin-release/atuin-${ATUIN_TARGET}/atuin" /usr/local/bin/atuin; \ atuin --version | grep -F "$ATUIN_VERSION"; \ rm -rf /tmp/atuin-release /tmp/atuin.tar.gz # ---------- Browser runtime (checksum-verified Bookworm security packages) ---------- RUN set -eux; \ case "$TARGETARCH" in amd64) DEB_ARCH=amd64; CHROMIUM_DEBIAN_VERSION="$CHROMIUM_DEBIAN_VERSION_AMD64"; CHROMIUM_PACKAGE_SHA256="$CHROMIUM_PACKAGE_SHA256_AMD64"; CHROMIUM_COMMON_PACKAGE_SHA256="$CHROMIUM_COMMON_PACKAGE_SHA256_AMD64"; CHROMIUM_SANDBOX_PACKAGE_SHA256="$CHROMIUM_SANDBOX_PACKAGE_SHA256_AMD64";; arm64) DEB_ARCH=arm64; CHROMIUM_DEBIAN_VERSION="$CHROMIUM_DEBIAN_VERSION_ARM64"; CHROMIUM_PACKAGE_SHA256="$CHROMIUM_PACKAGE_SHA256_ARM64"; CHROMIUM_COMMON_PACKAGE_SHA256="$CHROMIUM_COMMON_PACKAGE_SHA256_ARM64"; CHROMIUM_SANDBOX_PACKAGE_SHA256="$CHROMIUM_SANDBOX_PACKAGE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac; \ mkdir -p /tmp/chromium-debs; \ cd /tmp/chromium-debs; \ apt-get update; \ apt-get download \ "chromium=${CHROMIUM_DEBIAN_VERSION}" \ "chromium-common=${CHROMIUM_DEBIAN_VERSION}" \ "chromium-sandbox=${CHROMIUM_DEBIAN_VERSION}"; \ printf '%s %s\n' \ "$CHROMIUM_PACKAGE_SHA256" "chromium_${CHROMIUM_DEBIAN_VERSION}_${DEB_ARCH}.deb" \ "$CHROMIUM_COMMON_PACKAGE_SHA256" "chromium-common_${CHROMIUM_DEBIAN_VERSION}_${DEB_ARCH}.deb" \ "$CHROMIUM_SANDBOX_PACKAGE_SHA256" "chromium-sandbox_${CHROMIUM_DEBIAN_VERSION}_${DEB_ARCH}.deb" \ | sha256sum -c -; \ apt-get install -y --no-install-recommends ./*.deb; \ test "$(dpkg-query -W -f='${Version}' chromium)" = "$CHROMIUM_DEBIAN_VERSION"; \ test "$(dpkg-query -W -f='${Version}' chromium-common)" = "$CHROMIUM_DEBIAN_VERSION"; \ test "$(dpkg-query -W -f='${Version}' chromium-sandbox)" = "$CHROMIUM_DEBIAN_VERSION"; \ cd /; \ rm -rf /tmp/chromium-debs /var/lib/apt/lists/* # ---------- fzf (official multi-arch release) ---------- RUN FZF_ARCH=$(case "$TARGETARCH" in amd64) echo "amd64";; arm64) echo "arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ FZF_ARCHIVE_SHA256=$(case "$TARGETARCH" in amd64) echo "$FZF_ARCHIVE_SHA256_AMD64";; arm64) echo "$FZF_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ FZF_ASSET="fzf-${FZF_VERSION}-linux_${FZF_ARCH}.tar.gz" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "/tmp/${FZF_ASSET}" \ "https://github.com/junegunn/fzf/releases/download/v${FZF_VERSION}/${FZF_ASSET}" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o /tmp/fzf-checksums.txt \ "https://github.com/junegunn/fzf/releases/download/v${FZF_VERSION}/fzf_${FZF_VERSION}_checksums.txt" && \ test "$(grep -F " ${FZF_ASSET}" /tmp/fzf-checksums.txt | cut -d' ' -f1)" = "$FZF_ARCHIVE_SHA256" && \ echo "$FZF_ARCHIVE_SHA256 /tmp/${FZF_ASSET}" | sha256sum -c - && \ tar -xzf "/tmp/${FZF_ASSET}" -C /usr/local/bin fzf && \ test "$(/usr/local/bin/fzf --version | awk '{print $1}')" = "$FZF_VERSION" && \ rm -f "/tmp/${FZF_ASSET}" /tmp/fzf-checksums.txt RUN rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub # ---------- bubblewrap setuid (Codex CLI sandbox on restricted kernels) ---------- RUN test -x /usr/bin/bwrap && chown root:root /usr/bin/bwrap && chmod 4755 /usr/bin/bwrap && test "$(stat -c '%a %u %g' /usr/bin/bwrap)" = "4755 0 0" # ---------- Full-only system packages ---------- RUN if [ "$VARIANT" = "full" ]; then \ apt-get update && apt-get install -y --no-install-recommends \ pandoc libvips-dev \ && rm -rf /var/lib/apt/lists/*; \ fi COPY --from=ffmpeg-security-builder /out/ffmpeg-security-backport /tmp/ffmpeg-security-backport RUN if [ "$VARIANT" = "full" ]; then \ cd /tmp/ffmpeg-security-backport && \ sha256sum -c SHA256SUMS && \ apt-get update && apt-get install -y --no-install-recommends ./*.deb && \ for package_name in ffmpeg libavcodec59 libavdevice59 libavfilter8 libavformat59 libavutil57 libpostproc56 libswresample4 libswscale6; do \ dpkg --compare-versions "$(dpkg-query -W -f='${Version}' "$package_name")" eq "$FFMPEG_BACKPORT_VERSION"; \ done && \ rm -rf /tmp/ffmpeg-security-backport /var/lib/apt/lists/*; \ else \ rm -rf /tmp/ffmpeg-security-backport; \ fi # ---------- Azure CLI (full only) ---------- RUN if [ "$VARIANT" = "full" ]; then \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o /tmp/azure-cli-install.sh https://aka.ms/InstallAzureCLIDeb && \ echo "$AZURE_CLI_INSTALLER_SHA256 /tmp/azure-cli-install.sh" | sha256sum -c - && \ sed -i 's/apt-get install --assume-yes azure-cli/apt-get install --assume-yes azure-cli=$AZURE_CLI_VERSION/' /tmp/azure-cli-install.sh && \ grep -Fqx ' apt-get install --assume-yes azure-cli=$AZURE_CLI_VERSION' /tmp/azure-cli-install.sh && \ bash /tmp/azure-cli-install.sh && \ test "$(dpkg-query -W -f='${Version}' azure-cli)" = "$AZURE_CLI_VERSION" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL \ -o "/tmp/pyjwt-${AZURE_CLI_PYJWT_VERSION}-py3-none-any.whl" \ "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-${AZURE_CLI_PYJWT_VERSION}-py3-none-any.whl" && \ echo "$AZURE_CLI_PYJWT_WHEEL_SHA256 /tmp/pyjwt-${AZURE_CLI_PYJWT_VERSION}-py3-none-any.whl" | sha256sum -c - && \ /opt/az/bin/python3 -m pip install --no-cache-dir --no-deps \ "/tmp/pyjwt-${AZURE_CLI_PYJWT_VERSION}-py3-none-any.whl" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL \ -o "/tmp/urllib3-${AZURE_CLI_URLLIB3_VERSION}-py3-none-any.whl" \ "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-${AZURE_CLI_URLLIB3_VERSION}-py3-none-any.whl" && \ echo "$AZURE_CLI_URLLIB3_WHEEL_SHA256 /tmp/urllib3-${AZURE_CLI_URLLIB3_VERSION}-py3-none-any.whl" | sha256sum -c - && \ /opt/az/bin/python3 -m pip install --no-cache-dir --no-deps \ "/tmp/urllib3-${AZURE_CLI_URLLIB3_VERSION}-py3-none-any.whl" && \ test "$(/opt/az/bin/python3 --version)" = "Python 3.14.7" && \ test "$(/opt/az/bin/python3 -c 'import cryptography; print(cryptography.__version__)')" = "48.0.1" && \ test "$(/opt/az/bin/python3 -c 'import importlib.metadata; print(importlib.metadata.version("PyJWT"))')" = "$AZURE_CLI_PYJWT_VERSION" && \ test "$(/opt/az/bin/python3 -c 'import urllib3; print(urllib3.__version__)')" = "$AZURE_CLI_URLLIB3_VERSION" && \ /opt/az/bin/python3 -m pip check && \ az version >/dev/null && \ rm -rf /tmp/azure-cli-install.sh "/tmp/pyjwt-${AZURE_CLI_PYJWT_VERSION}-py3-none-any.whl" "/tmp/urllib3-${AZURE_CLI_URLLIB3_VERSION}-py3-none-any.whl" /var/lib/apt/lists/*; \ fi # ---------- GitHub CLI ---------- RUN GITHUB_CLI_ARCH=$(case "$TARGETARCH" in amd64) echo "amd64";; arm64) echo "arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ GITHUB_CLI_PACKAGE_SHA256=$(case "$TARGETARCH" in amd64) echo "$GITHUB_CLI_PACKAGE_SHA256_AMD64";; arm64) echo "$GITHUB_CLI_PACKAGE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ GITHUB_CLI_PACKAGE="gh_${GITHUB_CLI_VERSION}_linux_${GITHUB_CLI_ARCH}.deb" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "/tmp/${GITHUB_CLI_PACKAGE}" \ "https://github.com/cli/cli/releases/download/v${GITHUB_CLI_VERSION}/${GITHUB_CLI_PACKAGE}" && \ echo "$GITHUB_CLI_PACKAGE_SHA256 /tmp/${GITHUB_CLI_PACKAGE}" | sha256sum -c - && \ apt-get update && apt-get install -y "/tmp/${GITHUB_CLI_PACKAGE}" && \ test "$(dpkg-query -W -f='${Version}' gh)" = "$GITHUB_CLI_VERSION" && \ rm -f "/tmp/${GITHUB_CLI_PACKAGE}" && \ rm -rf /var/lib/apt/lists/* # ---------- bat symlink (Debian names it batcat) ---------- RUN ln -sf /usr/bin/batcat /usr/local/bin/bat 2>/dev/null || true # ---------- Locale configuration ---------- RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen # ---------- Create claude user ---------- # The official Node slim image already has UID 1000 as 'node' — rename it to 'claude' RUN usermod -l claude -d /home/claude -m node && \ groupmod -n claude node && \ mkdir -p /home/claude/.cloudcli && \ chown claude:claude /home/claude/.cloudcli && \ echo "claude ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/claude && \ chmod 0440 /etc/sudoers.d/claude # ---------- Claude Code CLI (native installer) ---------- # CRITICAL: WORKDIR must be non-root-owned or the installer hangs WORKDIR /workspace USER claude RUN CLAUDE_BINARY_SHA256=$(case "$TARGETARCH" in amd64) echo "$CLAUDE_BINARY_SHA256_AMD64";; arm64) echo "$CLAUDE_BINARY_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o /tmp/claude-install.sh https://claude.ai/install.sh && \ echo "$CLAUDE_INSTALLER_SHA256 /tmp/claude-install.sh" | sha256sum -c - && \ bash /tmp/claude-install.sh "$CLAUDE_CODE_VERSION" && \ test "$(/home/claude/.local/bin/claude --version | awk '{print $1}')" = "$CLAUDE_CODE_VERSION" && \ echo "$CLAUDE_BINARY_SHA256 $(readlink -f /home/claude/.local/bin/claude)" | sha256sum -c - && \ rm -f /tmp/claude-install.sh USER root RUN rm -f /home/claude/.claude.json ENV PATH="/home/claude/.local/bin:${PATH}" # ---------- npm global packages (slim — always installed) ---------- RUN npm install -g npm@12.2.1 && \ test "$(npm --version)" = "12.2.1" RUN PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -g \ playwright@1.64.0 \ typescript@7.0.2 tsx@4.23.15 \ pnpm@12.14.0 \ vite@8.3.5 esbuild@0.28.2 \ eslint@10.13.0 prettier@3.9.10 \ serve@14.2.6 nodemon@3.1.14 concurrently@10.0.6 \ dotenv-cli@11.0.0 # ---------- npm global packages (full only) ---------- RUN if [ "$VARIANT" = "full" ]; then \ set -e; \ npm i -g \ wrangler@4.151.3 vercel@63.1.2 netlify-cli@27.14.1 \ pm2@7.1.0 \ prisma@7.10.1 drizzle-kit@0.31.11 \ eas-cli@24.14.0 \ lighthouse@13.5.0 @lhci/cli@0.15.1 \ sharp-cli@6.1.0 json-server@0.17.4 http-server@14.1.1 \ @marp-team/marp-cli@4.5.1 && \ npm i -g --legacy-peer-deps @cloudflare/next-on-pages@1.13.16 && \ test "$(json-server --version)" = "0.17.4" && \ printf '%s\n' '{"posts":[{"id":1,"title":"smoke"}]}' > /tmp/json-server-smoke.json; \ json-server --watch /tmp/json-server-smoke.json --host 127.0.0.1 --port 3999 >/tmp/json-server-smoke.log 2>&1 & \ JSON_SERVER_PID=$! && \ for attempt in 1 2 3 4 5 6 7 8 9 10; do \ curl -fsS http://127.0.0.1:3999/posts/1 -o /tmp/json-server-response.json && break; \ sleep 1; \ done && \ node -e "const value = require('/tmp/json-server-response.json'); if (value.id !== 1 || value.title !== 'smoke') process.exit(1)" && \ kill "$JSON_SERVER_PID" && \ { \ wait "$JSON_SERVER_PID" 2>/dev/null || true; \ } && \ rm -f /tmp/json-server-smoke.json /tmp/json-server-response.json /tmp/json-server-smoke.log; \ fi # Prisma 7.10.1 now carries the fixed mysql2 release directly. Assert the # upstream dependency tree instead of maintaining a downstream source overlay. RUN if [ "$VARIANT" = "full" ]; then \ PRISMA_ROOT=/usr/local/lib/node_modules/prisma && \ MYSQL2_ROOT="$PRISMA_ROOT/node_modules/mysql2" && \ test "$(node -p "require('$PRISMA_ROOT/package.json').version")" = "7.10.1" && \ test "$(node -p "require('$PRISMA_ROOT/package.json').dependencies.mysql2")" = "3.24.4" && \ test "$(node -p "require('$MYSQL2_ROOT/package.json').version")" = "3.24.4" && \ node -e "if (typeof require('$MYSQL2_ROOT').createConnection !== 'function') throw new Error('invalid Prisma mysql2 module')" && \ npm --prefix "$PRISMA_ROOT" ls mysql2 --all >/dev/null; \ fi # Rebuild the exact esbuild versions retained by full-only tools with the # pinned Go toolchain, replacing old upstream native executables only. COPY --from=esbuild-builder /out/0.15.18/esbuild /tmp/esbuild-0.15.18 COPY --from=esbuild-builder /out/0.18.20/esbuild /tmp/esbuild-0.18.20 COPY --from=esbuild-builder /out/0.25.12/esbuild /tmp/esbuild-0.25.12 RUN if [ "$VARIANT" = "full" ]; then \ ESBUILD_PACKAGE_ARCH=$(case "$TARGETARCH" in amd64) echo "linux-x64";; arm64) echo "linux-arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ NEXT_ON_PAGES_ESBUILD_PACKAGE=$(case "$TARGETARCH" in amd64) echo "esbuild-linux-64";; arm64) echo "esbuild-linux-arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ NEXT_ON_PAGES_ESBUILD_ROOT="/usr/local/lib/node_modules/@cloudflare/next-on-pages/node_modules/${NEXT_ON_PAGES_ESBUILD_PACKAGE}" && \ test "$(node -p "require('${NEXT_ON_PAGES_ESBUILD_ROOT}/package.json').version")" = "0.15.18" && \ install -m 0755 /tmp/esbuild-0.15.18 \ /usr/local/lib/node_modules/@cloudflare/next-on-pages/node_modules/esbuild/bin/esbuild && \ install -m 0755 /tmp/esbuild-0.15.18 \ "${NEXT_ON_PAGES_ESBUILD_ROOT}/bin/esbuild" && \ install -m 0755 /tmp/esbuild-0.18.20 \ "/usr/local/lib/node_modules/drizzle-kit/node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/${ESBUILD_PACKAGE_ARCH}/bin/esbuild" && \ install -m 0755 /tmp/esbuild-0.25.12 \ "/usr/local/lib/node_modules/drizzle-kit/node_modules/@esbuild/${ESBUILD_PACKAGE_ARCH}/bin/esbuild" && \ test "$(sha256sum /tmp/esbuild-0.15.18 | cut -d' ' -f1)" = "$(sha256sum "${NEXT_ON_PAGES_ESBUILD_ROOT}/bin/esbuild" | cut -d' ' -f1)" && \ test "$(/usr/local/lib/node_modules/@cloudflare/next-on-pages/node_modules/esbuild/bin/esbuild --version)" = "0.15.18" && \ test "$("${NEXT_ON_PAGES_ESBUILD_ROOT}/bin/esbuild" --version)" = "0.15.18" && \ test "$(/usr/local/lib/node_modules/drizzle-kit/node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/${ESBUILD_PACKAGE_ARCH}/bin/esbuild --version)" = "0.18.20" && \ test "$(/usr/local/lib/node_modules/drizzle-kit/node_modules/@esbuild/${ESBUILD_PACKAGE_ARCH}/bin/esbuild --version)" = "0.25.12"; \ fi && \ rm -f /tmp/esbuild-0.15.18 /tmp/esbuild-0.18.20 /tmp/esbuild-0.25.12 # ---------- Python packages (slim — always installed) ---------- RUN pip install --no-cache-dir --break-system-packages \ requests==2.34.2 httpx==0.28.1 beautifulsoup4==4.15.0 lxml==6.1.3 \ Pillow==12.3.0 \ pandas==3.0.6 numpy==2.5.4 \ openpyxl==3.1.5 python-docx==1.2.0 \ jinja2==3.1.6 pyyaml==6.0.3 python-dotenv==1.2.4 markdown==3.11.2 \ rich==15.0.0 click==8.5.0 tqdm==4.70.1 \ desloppify==1.0 bandit==1.9.4 defusedxml==0.7.1 \ tree-sitter==0.26.0 tree-sitter-language-pack==1.22.1 stevedore==5.9.1 \ playwright==1.64.0 \ apprise==2.0.1 \ pytest==9.1.1 pytest-asyncio==1.4.0 flake8==7.4.1 \ aiomqtt==2.5.1 aiohttp==3.14.5 COPY scripts/holyclaude-chromium /usr/local/bin/holyclaude-chromium RUN CHROMIUM_DEBIAN_VERSION=$(case "$TARGETARCH" in amd64) echo "$CHROMIUM_DEBIAN_VERSION_AMD64";; arm64) echo "$CHROMIUM_DEBIAN_VERSION_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ test "$(dpkg-query -W -f='${Version}' chromium)" = "$CHROMIUM_DEBIAN_VERSION" && \ test "$(dpkg-query -W -f='${Version}' chromium-common)" = "$CHROMIUM_DEBIAN_VERSION" && \ test "$(dpkg-query -W -f='${Version}' chromium-sandbox)" = "$CHROMIUM_DEBIAN_VERSION" && \ test -x /usr/lib/chromium/chromium && \ chmod +x /usr/local/bin/holyclaude-chromium && \ ln -sf /usr/local/bin/holyclaude-chromium /usr/bin/chromium && \ test "$(node -p "require('/usr/local/lib/node_modules/playwright/package.json').version")" = "1.64.0" && \ test "$(python3 -c "import importlib.metadata; print(importlib.metadata.version('playwright'))")" = "1.64.0" && \ test "$(/usr/bin/chromium --version | awk '{print $2}')" = "${CHROMIUM_DEBIAN_VERSION%%-*}" && \ runuser -u claude -- test -r /usr/lib/chromium/chromium && \ runuser -u claude -- test -x /usr/lib/chromium/chromium && \ runuser -u claude -- /usr/bin/chromium --version # ---------- Python packages (full only) ---------- RUN if [ "$VARIANT" = "full" ]; then \ pip install --no-cache-dir --break-system-packages \ reportlab==5.0.1 weasyprint==70.0 cairosvg==2.9.1 fpdf2==2.8.9 PyMuPDF==1.28.2 img2pdf==0.6.3 \ xlsxwriter==3.3.0 xlrd==2.0.2 \ matplotlib==3.11.2 seaborn==0.13.2 \ python-pptx==1.0.2 \ fastapi==0.143.1 uvicorn==0.54.0; \ fi # Replace Bookworm's runtime setuptools copy after all image packages are built. RUN curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "/tmp/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" \ "https://files.pythonhosted.org/packages/ef/55/6dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" && \ echo "$SETUPTOOLS_WHEEL_SHA256 /tmp/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" | sha256sum -c - && \ pip install --no-cache-dir --no-deps --break-system-packages "/tmp/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" && \ rm -rf /usr/lib/python3/dist-packages/setuptools \ /usr/lib/python3/dist-packages/setuptools-66.1.1.egg-info \ /usr/lib/python3/dist-packages/pkg_resources \ /usr/lib/python3/dist-packages/_distutils_hack && \ test "$(python3 -c "import importlib.metadata; print(importlib.metadata.version('setuptools'))")" = "$SETUPTOOLS_VERSION" && \ python3 -m pip --version >/dev/null && \ rm -f "/tmp/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" # ---------- AI CLI providers ---------- RUN npm i -g @google/gemini-cli@0.65.0 @openai/codex@0.163.0 task-master-ai@0.43.1 USER claude RUN CURSOR_ASSET_ARCH=$(case "$TARGETARCH" in amd64) echo "x64";; arm64) echo "arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ CURSOR_ARCHIVE_SHA256=$(case "$TARGETARCH" in amd64) echo "$CURSOR_ARCHIVE_SHA256_AMD64";; arm64) echo "$CURSOR_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ CURSOR_NODE_SHA256=$(case "$TARGETARCH" in amd64) echo "$CURSOR_NODE_SHA256_AMD64";; arm64) echo "$CURSOR_NODE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ CURSOR_DIR="/home/claude/.local/share/cursor-agent/versions/$CURSOR_BUILD_ID" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o /tmp/cursor-agent.tar.gz "https://downloads.cursor.com/lab/${CURSOR_BUILD_ID}/linux/${CURSOR_ASSET_ARCH}/agent-cli-package.tar.gz" && \ echo "$CURSOR_ARCHIVE_SHA256 /tmp/cursor-agent.tar.gz" | sha256sum -c - && \ test "$(tar -tzf /tmp/cursor-agent.tar.gz | cut -d/ -f1 | sort -u)" = "dist-package" && \ tar -tzf /tmp/cursor-agent.tar.gz | grep -Fxq 'dist-package/cursor-agent' && \ tar -tzf /tmp/cursor-agent.tar.gz | grep -Fxq 'dist-package/node' && \ rm -rf "$CURSOR_DIR" && \ mkdir -p "$CURSOR_DIR" /home/claude/.local/bin && \ tar --strip-components=1 -xzf /tmp/cursor-agent.tar.gz -C "$CURSOR_DIR" && \ ln -sfn "$CURSOR_DIR/cursor-agent" /home/claude/.local/bin/agent && \ ln -sfn "$CURSOR_DIR/cursor-agent" /home/claude/.local/bin/cursor-agent && \ ln -sfn "$CURSOR_DIR/cursor-agent" /home/claude/.local/bin/cursor && \ echo "$CURSOR_LAUNCHER_SHA256 $CURSOR_DIR/cursor-agent" | sha256sum -c - && \ echo "$CURSOR_NODE_SHA256 $CURSOR_DIR/node" | sha256sum -c - && \ ! grep -aFq -- '--permission' "$CURSOR_DIR/cursor-agent" && \ ! grep -aFq -- '--allow-fs-read' "$CURSOR_DIR/cursor-agent" && \ ! grep -aFq -- '--allow-fs-write' "$CURSOR_DIR/cursor-agent" && \ rm -f "$CURSOR_DIR/node" && \ ln -s /usr/local/bin/node "$CURSOR_DIR/node" && \ test "$(readlink -f "$CURSOR_DIR/node")" = "$(readlink -f /usr/local/bin/node)" && \ test "$("$CURSOR_DIR/node" --version)" = "v26.12.1" && \ test "$(cursor-agent --version)" = "$CURSOR_BUILD_ID" && \ cursor-agent --help >/dev/null && \ rm -f /tmp/cursor-agent.tar.gz USER root # ---------- Junie CLI (full only) ---------- USER claude RUN if [ "$VARIANT" = "full" ]; then \ JUNIE_PLATFORM=$(case "$TARGETARCH" in amd64) echo "amd64";; arm64) echo "aarch64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ JUNIE_ARCHIVE_SHA256=$(case "$TARGETARCH" in amd64) echo "$JUNIE_ARCHIVE_SHA256_AMD64";; arm64) echo "$JUNIE_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ JUNIE_ARCHIVE="junie-nightly-${JUNIE_VERSION}-linux-${JUNIE_PLATFORM}.zip" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "/tmp/${JUNIE_ARCHIVE}" "https://github.com/jetbrains-junie/junie/releases/download/${JUNIE_VERSION}/${JUNIE_ARCHIVE}" && \ echo "$JUNIE_ARCHIVE_SHA256 /tmp/${JUNIE_ARCHIVE}" | sha256sum -c - && \ JUNIE_TARGET="/home/claude/.local/share/junie/versions/$JUNIE_VERSION" && \ JUNIE_STAGING="/home/claude/.local/share/junie/versions/.${JUNIE_VERSION}.tmp" && \ rm -rf "$JUNIE_STAGING" "$JUNIE_TARGET" && \ mkdir -p "$JUNIE_STAGING" /home/claude/.local/bin && \ JUNIE_TOP_LEVEL=$(unzip -Z1 "/tmp/${JUNIE_ARCHIVE}" | cut -d/ -f1 | sort -u | tr '\n' ' ') && \ test "$JUNIE_TOP_LEVEL" = "channel junie junie-app shim " && \ unzip -q "/tmp/${JUNIE_ARCHIVE}" 'junie-app/*' -d "$JUNIE_STAGING" && \ test -x "$JUNIE_STAGING/junie-app/bin/junie" && \ test -f "$JUNIE_STAGING/junie-app/lib/app/junie-nightly-${JUNIE_VERSION}-linux-${JUNIE_PLATFORM}.jar" && \ mv "$JUNIE_STAGING/junie-app" "$JUNIE_TARGET" && \ rmdir "$JUNIE_STAGING" && \ ln -sfn "$JUNIE_TARGET" /home/claude/.local/share/junie/current && \ ln -sfn /home/claude/.local/share/junie/current/bin/junie /home/claude/.local/bin/junie && \ test "$(readlink /home/claude/.local/share/junie/current)" = "/home/claude/.local/share/junie/versions/$JUNIE_VERSION" && \ /home/claude/.local/bin/junie --version >/dev/null && \ rm -f "/tmp/${JUNIE_ARCHIVE}"; \ fi USER root # ---------- OpenCode CLI (full only) ---------- RUN if [ "$VARIANT" = "full" ]; then \ npm i -g --allow-scripts=opencode-ai opencode-ai@1.19.3; \ test "$(opencode --version)" = "1.19.3"; \ fi # ---------- Pi Coding Agent (full only) ---------- RUN if [ "$VARIANT" = "full" ]; then \ npm i -g --ignore-scripts @earendil-works/pi-coding-agent@0.85.1; \ fi # Replace compatible vulnerable transitive packages without changing tool majors. COPY scripts/patch-global-node-security-dependencies.mjs /tmp/patch-global-node-security-dependencies.mjs RUN set -eux; \ replace_node_module() { \ package_name="$1"; \ package_version="$2"; \ archive_sha256="$3"; \ shift 3; \ archive="/tmp/${package_name}-${package_version}.tgz"; \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "$archive" "https://registry.npmjs.org/${package_name}/-/${package_name}-${package_version}.tgz"; \ echo "$archive_sha256 $archive" | sha256sum -c -; \ test "$(tar -tzf "$archive" | cut -d/ -f1 | sort -u)" = "package"; \ for target in "$@"; do \ rm -rf "$target"; \ mkdir -p "$target"; \ tar -xzf "$archive" --strip-components=1 -C "$target"; \ test "$(node -p "require('${target}/package.json').name")" = "$package_name"; \ test "$(node -p "require('${target}/package.json').version")" = "$package_version"; \ done; \ rm -f "$archive"; \ }; \ replace_scoped_node_module() { \ package_name="$1"; \ package_version="$2"; \ archive_sha256="$3"; \ archive_name="$4"; \ shift 4; \ archive="/tmp/${archive_name}-${package_version}.tgz"; \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "$archive" "https://registry.npmjs.org/${package_name}/-/${archive_name}-${package_version}.tgz"; \ echo "$archive_sha256 $archive" | sha256sum -c -; \ test "$(tar -tzf "$archive" | cut -d/ -f1 | sort -u)" = "package"; \ for target in "$@"; do \ rm -rf "$target"; \ mkdir -p "$target"; \ tar -xzf "$archive" --strip-components=1 -C "$target"; \ test "$(node -p "require('${target}/package.json').name")" = "$package_name"; \ test "$(node -p "require('${target}/package.json').version")" = "$package_version"; \ done; \ rm -f "$archive"; \ }; \ node /tmp/patch-global-node-security-dependencies.mjs --root / --variant "$VARIANT" --check-baseline; \ replace_node_module piscina "$PISCINA_VERSION" "$PISCINA_ARCHIVE_SHA256" \ "/home/claude/.local/share/cursor-agent/versions/$CURSOR_BUILD_ID/node_modules/piscina"; \ if [ "$VARIANT" = "full" ]; then \ replace_node_module undici "$UNDICI_8_VERSION" "$UNDICI_8_ARCHIVE_SHA256" \ /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/node_modules/undici; \ replace_scoped_node_module "@xmldom/xmldom" "$FULL_XMLDOM_VERSION" "$FULL_XMLDOM_ARCHIVE_SHA256" xmldom \ /usr/local/lib/node_modules/@marp-team/marp-cli/node_modules/@xmldom/xmldom; \ replace_node_module ws "$WS_VERSION" "$WS_ARCHIVE_SHA256" \ /usr/local/lib/node_modules/@cloudflare/next-on-pages/node_modules/ws; \ fi; \ node /tmp/patch-global-node-security-dependencies.mjs --root / --variant "$VARIANT"; \ test "$(npm --version)" = "12.2.1"; \ test "$(cursor-agent --version)" = "$CURSOR_BUILD_ID"; \ if [ "$VARIANT" = "full" ]; then \ npm --prefix /usr/local/lib/node_modules/wrangler ls undici --all >/dev/null; \ npm --prefix /usr/local/lib/node_modules/@earendil-works/pi-coding-agent ls undici --all >/dev/null; \ npm --prefix /usr/local/lib/node_modules/eas-cli ls nanoid --all >/dev/null; \ npm --prefix /usr/local/lib/node_modules/eas-cli ls joi --all >/dev/null; \ npm --prefix /usr/local/lib/node_modules/pm2 ls js-yaml --all >/dev/null; \ test "$(node -p "require('/usr/local/lib/node_modules/pm2/package.json').dependencies['js-yaml']")" = "4.3.2"; \ test "$(node -p "require('/usr/local/lib/node_modules/pm2/node_modules/js-yaml/package.json').version")" = "4.3.2"; \ npm --prefix /usr/local/lib/node_modules/vercel ls smol-toml --all >/dev/null; \ VERCEL_NATIVE_ARCH=$(case "$TARGETARCH" in amd64) echo x64;; arm64) echo arm64;; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac); \ VERCEL_NATIVE_ROOT="/usr/local/lib/node_modules/vercel/node_modules/@vercel/vc-native-linux-${VERCEL_NATIVE_ARCH}"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/package.json').version")" = "63.1.2"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/@vercel/container/package.json').version")" = "18.0.1"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/@vercel/container/package.json').dependencies.tar")" = "7.5.11"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/@vercel/fun/package.json').version")" = "1.3.6"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/@vercel/fun/package.json').dependencies.tar")" = "7.5.22"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/@vercel/python-analysis/package.json').version")" = "0.14.0"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/@vercel/rust/package.json').version")" = "19.0.1"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/@renovatebot/pep440/package.json').version")" = "4.2.1"; \ test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/smol-toml/package.json').version")" = "1.5.2"; \ test "$(node -p "require('${VERCEL_NATIVE_ROOT}/package.json').version")" = "63.1.2"; \ "$VERCEL_NATIVE_ROOT/bin/vercel" --version | grep -Fx '63.1.2'; \ node -e "const owner = '/usr/local/lib/node_modules/vercel/node_modules/@vercel/container'; const resolved = require.resolve('smol-toml', { paths: [owner] }); if (!resolved.startsWith('/usr/local/lib/node_modules/vercel/node_modules/smol-toml/')) throw new Error('unexpected @vercel/container smol-toml resolution: ' + resolved); if (typeof require(resolved).parse !== 'function') throw new Error('invalid @vercel/container smol-toml module');"; \ npm --prefix /usr/local/lib/node_modules/@marp-team/marp-cli ls @xmldom/xmldom --all >/dev/null; \ npm --prefix /usr/local/lib/node_modules/wrangler ls sharp --all >/dev/null; \ npm --prefix /usr/local/lib/node_modules/netlify-cli ls sharp --all >/dev/null; \ test "$(node -p "require('/usr/local/lib/node_modules/netlify-cli/package.json').version")" = "27.14.1"; \ test "$(node -p "require('/usr/local/lib/node_modules/@marp-team/marp-cli/node_modules/@xmldom/xmldom/package.json').version")" = "$FULL_XMLDOM_VERSION"; \ node -e "const { DOMImplementation, XMLSerializer } = require('/usr/local/lib/node_modules/@marp-team/marp-cli/node_modules/@xmldom/xmldom'); const implementation = new DOMImplementation(); const doctype = implementation.createDocumentType('html', '', ''); doctype.name = 'html> { const sharp = require('/usr/local/lib/node_modules/wrangler/node_modules/sharp'); const buffer = await sharp({ create: { width: 2, height: 2, channels: 4, background: '#336699ff' } }).png().toBuffer(); if (buffer.subarray(1, 4).toString() !== 'PNG') throw new Error('invalid sharp PNG transform'); console.log('sharp_transform=ok'); })().catch((error) => { console.error(error); process.exit(1); })"; \ timeout 5s node -e "const { parse } = require('/usr/local/lib/node_modules/vercel/node_modules/smol-toml'); const value = parse('project = \\\"holyclaude\\\"\\n[build]\\ncommand = \\\"npm run build\\\"'); if (value.project !== 'holyclaude' || value.build.command !== 'npm run build') throw new Error('invalid TOML parse'); console.log('vercel_smol_toml=ok')"; \ node -e "const Joi = require('/usr/local/lib/node_modules/eas-cli/node_modules/joi'); const schema = Joi.object({ platform: Joi.string().valid('android', 'ios').required(), profile: Joi.string().min(1).required() }); const valid = schema.validate({ platform: 'android', profile: 'production' }); if (valid.error) throw valid.error; const invalid = schema.validate({ platform: 'desktop', profile: '' }); if (!invalid.error) throw new Error('Joi accepted an invalid EAS profile'); console.log('eas_joi_validation=ok')"; \ timeout 5s node -e "const { mkdtempSync, mkdirSync, rmSync, writeFileSync } = require('node:fs'); const { tmpdir } = require('node:os'); const { join } = require('node:path'); (async () => { const root = mkdtempSync(join(tmpdir(), 'vercel-smol-toml-')); try { const python = join(root, 'python'); mkdirSync(python); writeFileSync(join(python, 'pyproject.toml'), '[project]\\nname = \\\"smoke\\\"\\nversion = \\\"0.1.0\\\"\\n'); const { discoverPythonPackage } = require('/usr/local/lib/node_modules/vercel/node_modules/@vercel/python-analysis'); const discovered = await discoverPythonPackage({ entrypointDir: python, rootDir: root }); if (discovered.manifest?.data?.project?.name !== 'smoke') throw new Error('python-analysis failed to parse pyproject.toml'); const rust = join(root, 'rust'); mkdirSync(join(rust, 'src'), { recursive: true }); writeFileSync(join(rust, 'Cargo.toml'), '[package]\\nname = \\\"smoke\\\"\\nversion = \\\"0.1.0\\\"\\n[dependencies]\\nvercel_runtime = \\\"1\\\"\\n'); writeFileSync(join(rust, 'src/main.rs'), 'fn main() {}\\n'); const { shouldServe } = require('/usr/local/lib/node_modules/vercel/node_modules/@vercel/rust'); if (await shouldServe({ workPath: rust, entrypoint: 'src/main.rs', requestPath: 'different' })) throw new Error('rust failed to parse Cargo.toml runtime dependency'); console.log('vercel_smol_toml_consumers=ok'); } finally { rmSync(root, { recursive: true, force: true }); } })().catch((error) => { console.error(error); process.exit(1); })"; \ test "$(node -p "require('/usr/local/lib/node_modules/netlify-cli/node_modules/sharp/package.json').version")" = "0.35.5"; \ node -e "(async () => { const sharp = require('/usr/local/lib/node_modules/netlify-cli/node_modules/sharp'); if (typeof sharp !== 'function' || typeof sharp.format !== 'object') throw new Error('invalid Netlify sharp exports'); const png = await sharp({ create: { width: 2, height: 2, channels: 4, background: '#336699ff' } }).png().toBuffer(); if (png.subarray(1, 4).toString() !== 'PNG') throw new Error('invalid Netlify sharp PNG transform'); const avif = await sharp(png).avif().toBuffer(); const metadata = await sharp(avif).metadata(); if (metadata.format !== 'heif' || metadata.width !== 2 || metadata.height !== 2) throw new Error('invalid Netlify sharp AVIF decode'); console.log('netlify_sharp_build=ok'); })().catch((error) => { console.error(error); process.exit(1); })"; \ wrangler --version >/dev/null; \ pi --version >/dev/null; \ PM2_HOME=/tmp/holyclaude-build-pm2 pm2 --version | grep -Fx "7.1.0"; \ PM2_HOME=/tmp/holyclaude-build-pm2 pm2 kill >/dev/null; \ rm -rf /tmp/holyclaude-build-pm2; \ eas --version >/dev/null; \ vercel --version >/dev/null; \ test "$(next-on-pages --version)" = "1.13.16"; \ sharp --version >/dev/null; \ fi; \ rm -f /tmp/patch-global-node-security-dependencies.mjs ARG CLOUDCLI_VERSION=1.37.3 ARG CLOUDCLI_ACCOUNT_MANAGEMENT_ARTIFACT=cloudcli-ai-cloudcli-1.37.3-holyclaude-account-management.tgz ARG CLOUDCLI_ACCOUNT_MANAGEMENT_ARTIFACT_SHA256=545b926cd4b48afe9aa20a544782f6160e8631fe77f51f12d3dd0c92e187ccb2 COPY vendor/artifacts/${CLOUDCLI_ACCOUNT_MANAGEMENT_ARTIFACT} /tmp/vendor/cloudcli-ai-cloudcli.tgz COPY vendor/artifacts/cloudcli-account-management.manifest.json /tmp/vendor/cloudcli-account-management.manifest.json COPY --chown=claude:claude vendor/locks/cloudcli-web-terminal-6757ed0ef067cf7d8e1bf20fa0dd64b97e61889d.package-lock.json /tmp/vendor/web-terminal-package-lock.json # ---------- CloudCLI (web UI for Claude Code) ---------- # Production image intentionally omits optional, non-exposed oxlint type-aware and type-check support; the reproducible build/development artifact remains unchanged. RUN echo "$CLOUDCLI_ACCOUNT_MANAGEMENT_ARTIFACT_SHA256 /tmp/vendor/cloudcli-ai-cloudcli.tgz" | sha256sum -c - && \ CLOUDCLI_ROOT=/usr/local/lib/node_modules/@cloudcli-ai/cloudcli && \ mkdir -p "$(dirname "$CLOUDCLI_ROOT")" /tmp/cloudcli-unpack && \ tar -xzf /tmp/vendor/cloudcli-ai-cloudcli.tgz -C /tmp/cloudcli-unpack && \ mv /tmp/cloudcli-unpack/package "$CLOUDCLI_ROOT" && \ cd "$CLOUDCLI_ROOT" && \ : "BEGIN CLOUDCLI_RIPGREP_CACHE" && \ CLOUDCLI_RIPGREP_TARGET="$(case "$TARGETARCH" in amd64) echo x86_64-unknown-linux-musl;; arm64) echo aarch64-unknown-linux-musl;; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac)" && \ CLOUDCLI_RIPGREP_ARCHIVE_SHA256="$(case "$TARGETARCH" in amd64) echo "$CLOUDCLI_RIPGREP_ARCHIVE_SHA256_AMD64";; arm64) echo "$CLOUDCLI_RIPGREP_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac)" && \ CLOUDCLI_RIPGREP_CACHE_ROOT="${CLOUDCLI_RIPGREP_CACHE_ROOT:-/tmp}" && \ CLOUDCLI_RIPGREP_CACHE_DIR="$CLOUDCLI_RIPGREP_CACHE_ROOT/vscode-ripgrep-cache-${CLOUDCLI_VSCODE_RIPGREP_PACKAGE_VERSION}" && \ CLOUDCLI_RIPGREP_ARCHIVE="ripgrep-v${CLOUDCLI_RIPGREP_RELEASE_VERSION}-${CLOUDCLI_RIPGREP_TARGET}.tar.gz" && \ rm -rf "$CLOUDCLI_RIPGREP_CACHE_DIR" && \ mkdir -p "$CLOUDCLI_RIPGREP_CACHE_DIR" && \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL \ -o "$CLOUDCLI_RIPGREP_CACHE_DIR/$CLOUDCLI_RIPGREP_ARCHIVE" \ "https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${CLOUDCLI_RIPGREP_RELEASE_VERSION}/${CLOUDCLI_RIPGREP_ARCHIVE}" && \ echo "$CLOUDCLI_RIPGREP_ARCHIVE_SHA256 $CLOUDCLI_RIPGREP_CACHE_DIR/$CLOUDCLI_RIPGREP_ARCHIVE" | sha256sum -c - && \ : "END CLOUDCLI_RIPGREP_CACHE" && \ CLOUDCLI_SHRINKWRAP_SHA256="$(sha256sum npm-shrinkwrap.json | cut -d' ' -f1)" && \ test "$(node -p "require('./npm-shrinkwrap.json').packages['node_modules/@vscode/ripgrep'].version")" = "$CLOUDCLI_VSCODE_RIPGREP_PACKAGE_VERSION" && \ cp -- npm-shrinkwrap.json package-lock.json && \ echo "$CLOUDCLI_SHRINKWRAP_SHA256 npm-shrinkwrap.json" | sha256sum -c - && \ echo "$CLOUDCLI_SHRINKWRAP_SHA256 package-lock.json" | sha256sum -c - && \ test "$(npm --version)" = "12.2.1" && \ TMPDIR="$CLOUDCLI_RIPGREP_CACHE_ROOT" npm ci --omit=dev --allow-remote=all --allow-file=none --allow-git=none --allow-directory=none && \ echo "$CLOUDCLI_SHRINKWRAP_SHA256 npm-shrinkwrap.json" | sha256sum -c - && \ cmp -s npm-shrinkwrap.json package-lock.json && \ rm -f package-lock.json && \ test "$(node -p "require('./node_modules/@vscode/ripgrep/package.json').version")" = "$CLOUDCLI_VSCODE_RIPGREP_PACKAGE_VERSION" && \ : "BEGIN CLOUDCLI_RIPGREP_VERSION_PROBE" && \ CLOUDCLI_RIPGREP_EXPECTED_VERSION="$(case "$TARGETARCH" in amd64) echo "ripgrep $CLOUDCLI_RIPGREP_BINARY_VERSION (rev $CLOUDCLI_RIPGREP_BINARY_REVISION_AMD64)";; arm64) echo "ripgrep $CLOUDCLI_RIPGREP_BINARY_VERSION";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac)" && \ CLOUDCLI_RIPGREP_VERSION_OUTPUT="$("$CLOUDCLI_ROOT/node_modules/@vscode/ripgrep/bin/rg" --version | sed -n '1p')" && \ if [ "$CLOUDCLI_RIPGREP_VERSION_OUTPUT" != "$CLOUDCLI_RIPGREP_EXPECTED_VERSION" ]; then \ echo "Unexpected CloudCLI ripgrep binary version: $CLOUDCLI_RIPGREP_VERSION_OUTPUT" >&2; exit 1; \ fi && \ : "END CLOUDCLI_RIPGREP_VERSION_PROBE" && \ test "$(node -p "require('./node_modules/oxlint-tsgolint/package.json').version")" = "7.0.2001" && \ CLOUDCLI_TSGOLINT_ARCH="$(case "$(dpkg --print-architecture)" in amd64) echo x64;; arm64) echo arm64;; *) exit 1;; esac)" && \ test "$(node -p "require('./node_modules/@oxlint-tsgolint/linux-${CLOUDCLI_TSGOLINT_ARCH}/package.json').version")" = "7.0.2001" && \ test -x "node_modules/@oxlint-tsgolint/linux-${CLOUDCLI_TSGOLINT_ARCH}/tsgolint" && \ test -L node_modules/.bin/tsgolint && \ test "$(readlink node_modules/.bin/tsgolint)" = "../oxlint-tsgolint/bin/tsgolint.js" && \ rm -rf node_modules/oxlint-tsgolint "node_modules/@oxlint-tsgolint/linux-${CLOUDCLI_TSGOLINT_ARCH}" && \ rmdir node_modules/@oxlint-tsgolint && \ rm -f node_modules/.bin/tsgolint && \ test ! -e node_modules/oxlint-tsgolint && test ! -L node_modules/oxlint-tsgolint && \ test ! -e node_modules/@oxlint-tsgolint && test ! -L node_modules/@oxlint-tsgolint && \ test ! -e node_modules/.bin/tsgolint && test ! -L node_modules/.bin/tsgolint && \ node --input-type=module -e "import { createRequire } from 'node:module'; const require = createRequire('file:///usr/local/lib/node_modules/@cloudcli-ai/cloudcli/package.json'); const Database = require('better-sqlite3'); const db = new Database(':memory:'); db.exec('CREATE TABLE smoke (id INTEGER)'); db.close();" && \ chmod 0755 "$CLOUDCLI_ROOT/dist-server/server/modules/cli/cli.js" && \ ln -s "$CLOUDCLI_ROOT/dist-server/server/modules/cli/cli.js" /usr/local/bin/cloudcli && \ test -x /usr/local/bin/cloudcli && \ rm -rf "$CLOUDCLI_RIPGREP_CACHE_DIR" /tmp/vendor/cloudcli-ai-cloudcli.tgz /tmp/cloudcli-unpack RUN set -eux; \ replace_nested_node_module() { \ package="$1"; version="$2"; expected_sha256="$3"; shift 3; \ archive="/tmp/${package}-${version}.tgz"; \ curl --disable --retry 8 --retry-all-errors --retry-max-time 300 --remove-on-error --connect-timeout 15 --max-time 300 -fsSL -o "$archive" "https://registry.npmjs.org/${package}/-/${package}-${version}.tgz"; \ echo "$expected_sha256 $archive" | sha256sum -c -; \ for target in "$@"; do \ rm -rf "$target"; mkdir -p "$target"; \ tar -xzf "$archive" -C "$target" --strip-components=1; \ test "$(node -p "require('$target/package.json').version")" = "$version"; \ done; \ rm -f "$archive"; \ }; \ replace_nested_node_module nanoid "$CLOUDCLI_NANOID_VERSION" "$CLOUDCLI_NANOID_ARCHIVE_SHA256" \ /usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/nanoid; \ replace_nested_node_module ip-address "$NESTED_IP_ADDRESS_VERSION" "$NESTED_IP_ADDRESS_ARCHIVE_SHA256" \ /usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/ip-address; \ replace_nested_node_module fast-uri "$CLOUDCLI_FAST_URI_VERSION" "$CLOUDCLI_FAST_URI_ARCHIVE_SHA256" \ /usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/fast-uri; \ test "$(node -p "require('/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/package.json').name")" = "@cloudcli-ai/cloudcli"; \ test "$(node -p "require('/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/package.json').version")" = "$CLOUDCLI_VERSION"; \ test "$(node -p "require('/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/gray-matter/package.json').version")" = "4.0.3"; \ test "$(node -p "require('/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/gray-matter/package.json').dependencies['js-yaml']")" = "^3.13.1"; \ test "$(node -p "require('/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/js-yaml/package.json').name")" = "js-yaml"; \ CLOUDCLI_JS_YAML_INSTALLED_VERSION="$(node -p "require('/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/js-yaml/package.json').version")"; \ case "$CLOUDCLI_JS_YAML_INSTALLED_VERSION" in \ 3.15.1|"$CLOUDCLI_JS_YAML_VERSION") ;; \ *) echo "Unexpected CloudCLI js-yaml version: $CLOUDCLI_JS_YAML_INSTALLED_VERSION" >&2; exit 1 ;; \ esac; \ replace_nested_node_module js-yaml "$CLOUDCLI_JS_YAML_VERSION" "$CLOUDCLI_JS_YAML_ARCHIVE_SHA256" \ /usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/js-yaml RUN npm --prefix /usr/local/lib/node_modules/@cloudcli-ai/cloudcli ls js-yaml --all >/dev/null && \ node -e "const matter = require('/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/gray-matter'); const value = matter('---\\ntitle: Secure\\nfeatures:\\n - overlays\\n---\\nBody'); if (value.data.title !== 'Secure' || value.data.features[0] !== 'overlays' || value.content.trim() !== 'Body') throw new Error('invalid gray-matter front matter')" && \ timeout 5s node -e "const yaml = require('/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/node_modules/js-yaml'); const source = 'arr: &arr [' + '{},'.repeat(12).slice(0, -1) + ']\\ntargets:\\n' + ' - <<: *arr\\n'.repeat(12); let rejected = false; try { yaml.load(source, { schema: yaml.DEFAULT_FULL_SCHEMA, maxTotalMergeKeys: 8 }); } catch (error) { if (error.reason !== 'merge keys exceeded maxTotalMergeKeys (8)') throw error; rejected = true; } if (!rejected) throw new Error('advisory input was accepted'); console.log('cloudcli_js_yaml_merge_limit=ok')" RUN node --input-type=module -e "import { createRequire } from 'node:module'; const require = createRequire('file:///usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/index.js'); const packagePath = require.resolve('playwright/package.json'); if (packagePath !== '/usr/local/lib/node_modules/playwright/package.json') throw new Error('CloudCLI resolved unexpected Playwright package: ' + packagePath); const { version } = require(packagePath); if (version !== '1.64.0') throw new Error('CloudCLI resolved unexpected Playwright version: ' + version);" && \ test -x /usr/bin/chromium COPY scripts/patch-cloudcli-apprise-notifications.mjs /tmp/patch-cloudcli-apprise-notifications.mjs COPY scripts/patch-cloudcli-base-path.mjs /tmp/patch-cloudcli-base-path.mjs COPY scripts/patch-cloudcli-browser-runtime.mjs /tmp/patch-cloudcli-browser-runtime.mjs COPY scripts/patch-cloudcli-codex-complete-exit-code.mjs /tmp/patch-cloudcli-codex-complete-exit-code.mjs COPY scripts/patch-cloudcli-codex-permissions.mjs /tmp/patch-cloudcli-codex-permissions.mjs COPY scripts/patch-cloudcli-disable-self-update.mjs /tmp/patch-cloudcli-disable-self-update.mjs COPY --chown=claude:claude scripts/patch-cloudcli-web-terminal-install-policy.mjs /tmp/patch-cloudcli-web-terminal-install-policy.mjs COPY --chown=claude:claude scripts/patch-cloudcli-web-terminal-rendering.mjs /tmp/patch-cloudcli-web-terminal-rendering.mjs COPY scripts/verify-cloudcli-account-management-support.mjs /tmp/verify-cloudcli-account-management-support.mjs RUN touch /usr/local/lib/node_modules/@cloudcli-ai/cloudcli/.env # patch: launch CloudCLI Browser Use with HolyClaude's canonical Chromium RUN node /tmp/patch-cloudcli-browser-runtime.mjs && rm -f /tmp/patch-cloudcli-browser-runtime.mjs RUN CLOUDCLI_BROWSER_USE="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/browser-use/browser-use.service.js" && \ grep -Fq "// HolyClaude canonical browser runtime" "$CLOUDCLI_BROWSER_USE" && \ grep -Fq "executablePath: process.env.CHROME_PATH," "$CLOUDCLI_BROWSER_USE" && \ grep -Fq "const executablePath = process.env.CHROME_PATH || playwright.chromium.executablePath();" "$CLOUDCLI_BROWSER_USE" && \ echo "[patch] CloudCLI Browser Use canonical Chromium applied to runtime" # patch: disable CloudCLI npm self-update inside HolyClaude (issue #50) RUN node /tmp/patch-cloudcli-disable-self-update.mjs && rm -f /tmp/patch-cloudcli-disable-self-update.mjs # CloudCLI 1.37.3 already contains the WebSocket binary-frame fix, provider # model flow, and final Codex complete exit codes. Keep checks fail-closed. RUN CLOUDCLI_WS_PROXY="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js" && \ grep -q "binary: isBinary" "$CLOUDCLI_WS_PROXY" && \ echo "[patch] WebSocket frame type fix already present upstream" RUN CLOUDCLI_COMMANDS="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/commands/commands.routes.js" && \ grep -q "modelsService.getProviderModels" "$CLOUDCLI_COMMANDS" && \ echo "[patch] Provider model command flow already present upstream" RUN CLOUDCLI_CODEX="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js" && \ grep -q "exitCode: terminalFailure ? 1 : 0" "$CLOUDCLI_CODEX" && \ grep -q "exitCode: 1" "$CLOUDCLI_CODEX" && \ echo "[patch] Codex final completion exitCode fix already present upstream" # patch: support serving CloudCLI below a reverse-proxy subpath (issue #64) RUN node /tmp/patch-cloudcli-base-path.mjs && rm -f /tmp/patch-cloudcli-base-path.mjs RUN CLOUDCLI_SERVER="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/index.js" && \ CLOUDCLI_WS_SERVER="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/websocket/services/websocket-server.service.js" && \ grep -q "HOLYCLAUDE_BASE_PATH" "$CLOUDCLI_SERVER" && \ grep -q "sendHolyClaudeIndexHtml" "$CLOUDCLI_SERVER" && \ grep -q "stripHolyClaudeBasePathFromPathname" "$CLOUDCLI_WS_SERVER" && \ echo "[patch] CloudCLI base path support applied to runtime" # patch: bridge Codex CloudCLI lifecycle events to Apprise (issue #17) RUN node /tmp/patch-cloudcli-apprise-notifications.mjs && rm -f /tmp/patch-cloudcli-apprise-notifications.mjs RUN CLOUDCLI_NOTIFICATIONS="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/notifications/services/notification-orchestrator.service.js" && \ test "$(grep -c "^ sendAppriseLifecycleNotification({" "$CLOUDCLI_NOTIFICATIONS")" = "2" && \ grep -q "kind: 'stop'" "$CLOUDCLI_NOTIFICATIONS" && \ grep -q "kind: 'error'" "$CLOUDCLI_NOTIFICATIONS" && \ echo "[patch] Apprise lifecycle bridge applied to CloudCLI runtime" # patch: configure Codex CloudCLI chat permission mode (issue #18) RUN node /tmp/patch-cloudcli-codex-permissions.mjs && rm -f /tmp/patch-cloudcli-codex-permissions.mjs # patch: preserve explicit Codex complete fields on the 1.35.x provider path (issue #19) RUN node /tmp/patch-cloudcli-codex-complete-exit-code.mjs && rm -f /tmp/patch-cloudcli-codex-complete-exit-code.mjs RUN CLOUDCLI_CODEX_PROVIDER="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js" && \ grep -q "exitCode: 0" "$CLOUDCLI_CODEX_PROVIDER" && \ grep -q "success: true" "$CLOUDCLI_CODEX_PROVIDER" && \ grep -q "aborted: false" "$CLOUDCLI_CODEX_PROVIDER" && \ echo "[patch] Codex provider completion fields applied to CloudCLI runtime" # patch: local account logout/password bridge for CloudCLI (issue #65) RUN node /tmp/verify-cloudcli-account-management-support.mjs /usr/local/lib/node_modules/@cloudcli-ai/cloudcli && \ rm -f /tmp/verify-cloudcli-account-management-support.mjs /tmp/vendor/cloudcli-account-management.manifest.json # ---------- CloudCLI plugins (baked into image) ---------- COPY --from=esbuild-builder --chown=claude:claude /out/0.25.12/esbuild /tmp/web-terminal-esbuild USER claude RUN mkdir -p /home/claude/.claude-code-ui/plugins && \ git init /home/claude/.claude-code-ui/plugins/project-stats && \ cd /home/claude/.claude-code-ui/plugins/project-stats && \ git remote add origin https://github.com/cloudcli-ai/cloudcli-plugin-starter.git && \ git fetch --depth 1 origin 4895cd3fd33362471e739b786493aba048487bcc && \ git checkout --detach FETCH_HEAD && \ test "$(git rev-parse --short=12 HEAD)" = "4895cd3fd333" && \ npm ci --strict-allow-scripts && npm run build && \ git init /home/claude/.claude-code-ui/plugins/web-terminal && \ cd /home/claude/.claude-code-ui/plugins/web-terminal && \ git remote add origin https://github.com/cloudcli-ai/cloudcli-plugin-terminal.git && \ git fetch --depth 1 origin 6757ed0ef067cf7d8e1bf20fa0dd64b97e61889d && \ git checkout --detach FETCH_HEAD && \ test "$(git rev-parse --short=12 HEAD)" = "6757ed0ef067" && \ cp /tmp/vendor/web-terminal-package-lock.json package-lock.json && \ node /tmp/patch-cloudcli-web-terminal-install-policy.mjs /home/claude/.claude-code-ui/plugins/web-terminal && \ node /tmp/patch-cloudcli-web-terminal-rendering.mjs /home/claude/.claude-code-ui/plugins/web-terminal && \ npm ci --strict-allow-scripts && \ ESBUILD_PACKAGE_ARCH=$(case "$TARGETARCH" in amd64) echo "x64";; arm64) echo "arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \ test "$(node -p "require('./node_modules/esbuild/package.json').version")" = "0.25.12" && \ test "$(node -p "require('./node_modules/@esbuild/linux-${ESBUILD_PACKAGE_ARCH}/package.json').version")" = "0.25.12" && \ install -m 0755 /tmp/web-terminal-esbuild node_modules/esbuild/bin/esbuild && \ install -m 0755 /tmp/web-terminal-esbuild "node_modules/@esbuild/linux-${ESBUILD_PACKAGE_ARCH}/bin/esbuild" && \ test "$(node_modules/esbuild/bin/esbuild --version)" = "0.25.12" && \ test "$(node_modules/@esbuild/linux-${ESBUILD_PACKAGE_ARCH}/bin/esbuild --version)" = "0.25.12" && \ test "$(sha256sum node_modules/esbuild/bin/esbuild | cut -d' ' -f1)" = "$(sha256sum "node_modules/@esbuild/linux-${ESBUILD_PACKAGE_ARCH}/bin/esbuild" | cut -d' ' -f1)" && \ node -e "require('node-pty')" && npm run build && \ echo '{"project-stats":{"name":"project-stats","source":"https://github.com/cloudcli-ai/cloudcli-plugin-starter","enabled":true},"web-terminal":{"name":"web-terminal","source":"https://github.com/cloudcli-ai/cloudcli-plugin-terminal","enabled":true}}' > /home/claude/.claude-code-ui/plugins.json USER root RUN rm -f /tmp/patch-cloudcli-web-terminal-install-policy.mjs /tmp/patch-cloudcli-web-terminal-rendering.mjs /tmp/vendor/web-terminal-package-lock.json /tmp/web-terminal-esbuild # ---------- Store variant for bootstrap ---------- RUN echo "${VARIANT}" > /etc/holyclaude-variant # ---------- Copy config files ---------- COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh COPY scripts/bootstrap.sh /usr/local/bin/bootstrap.sh COPY scripts/holyclaude-mosh-server /usr/local/bin/holyclaude-mosh-server COPY scripts/persist-claude-json.mjs /usr/local/bin/persist-claude-json.mjs COPY scripts/prepare-cli-persistence.sh /usr/local/bin/prepare-cli-persistence.sh COPY scripts/secure-cli-persistence.py /usr/local/bin/secure-cli-persistence.py COPY scripts/notify.py /usr/local/bin/notify.py COPY config/settings.json /usr/local/share/holyclaude/settings.json COPY config/claude-memory-full.md /usr/local/share/holyclaude/claude-memory-full.md COPY config/claude-memory-slim.md /usr/local/share/holyclaude/claude-memory-slim.md RUN chmod +x /usr/local/bin/entrypoint.sh \ /usr/local/bin/bootstrap.sh \ /usr/local/bin/holyclaude-mosh-server \ /usr/local/bin/persist-claude-json.mjs \ /usr/local/bin/prepare-cli-persistence.sh \ /usr/local/bin/secure-cli-persistence.py \ /usr/local/bin/notify.py RUN mkdir -p /usr/local/lib/holyclaude && \ if [ -x /usr/bin/mosh-server ]; then \ mv /usr/bin/mosh-server /usr/local/lib/holyclaude/mosh-server.real && \ ln -sf /usr/local/bin/holyclaude-mosh-server /usr/bin/mosh-server; \ fi # ---------- s6-overlay service definitions ---------- COPY s6-overlay/s6-rc.d/cloudcli/type /etc/s6-overlay/s6-rc.d/cloudcli/type COPY s6-overlay/s6-rc.d/cloudcli/run /etc/s6-overlay/s6-rc.d/cloudcli/run COPY s6-overlay/s6-rc.d/persist-claude-json/type /etc/s6-overlay/s6-rc.d/persist-claude-json/type COPY s6-overlay/s6-rc.d/persist-claude-json/run /etc/s6-overlay/s6-rc.d/persist-claude-json/run COPY s6-overlay/s6-rc.d/xvfb/type /etc/s6-overlay/s6-rc.d/xvfb/type COPY s6-overlay/s6-rc.d/xvfb/run /etc/s6-overlay/s6-rc.d/xvfb/run COPY s6-overlay/s6-rc.d/sshd/type /etc/s6-overlay/s6-rc.d/sshd/type COPY s6-overlay/s6-rc.d/sshd/run /etc/s6-overlay/s6-rc.d/sshd/run RUN chmod +x /etc/s6-overlay/s6-rc.d/cloudcli/run \ /etc/s6-overlay/s6-rc.d/persist-claude-json/run \ /etc/s6-overlay/s6-rc.d/xvfb/run \ /etc/s6-overlay/s6-rc.d/sshd/run && \ touch /etc/s6-overlay/user-bundles.d/user/contents.d/cloudcli && \ touch /etc/s6-overlay/user-bundles.d/user/contents.d/persist-claude-json && \ touch /etc/s6-overlay/user-bundles.d/user/contents.d/xvfb # ---------- Working directory ---------- WORKDIR /workspace # ---------- Health check ---------- HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD curl -sf http://localhost:3001/ || exit 1 # ---------- s6-overlay as PID 1 ---------- ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]