FROM python:3.14-slim-trixie AS base

ARG TARGETARCH

# Shared Node.js, containment, gateway-client, and cloudflared baseline for
# the first-party Python-and-Node workload images.
COPY --from=shared install-workload-base.sh /usr/local/lib/agent/install-workload-base.sh
RUN TARGETARCH="$TARGETARCH" bash /usr/local/lib/agent/install-workload-base.sh

# Optional profile-managed organisational CAs. The canonical context carries
# only a marker; onboarding replaces this directory with the selected file or
# certificate directory before building. The merged system bundle is then
# available to curl/git/OpenSSL clients on the read-only runtime rootfs.
COPY proxy-ca.d/ /tmp/agent-containers-proxy-ca/
RUN set -eu; \
    mkdir -p /usr/local/share/ca-certificates/custom; \
    found=0; \
    for cert in /tmp/agent-containers-proxy-ca/*; do \
        [ -f "$cert" ] || continue; \
        name="$(basename "$cert")"; \
        [ "$name" = ".keep" ] && continue; \
        case "$name" in *.crt) ;; *) name="${name%.*}.crt" ;; esac; \
        cp "$cert" "/usr/local/share/ca-certificates/custom/$name"; \
        found=1; \
    done; \
    if [ "$found" -eq 1 ]; then update-ca-certificates; fi; \
    rm -rf /tmp/agent-containers-proxy-ca

# User-configurable default software (gh, jq, ripgrep, ...) — see
# packages-apt.txt. Kept as a separate RUN from the baseline block above so
# the required-infra list stays untouched by editing that file.
COPY --from=shared install-additional-packages.sh /usr/local/lib/agent/install-additional-packages.sh
COPY packages-apt.txt /tmp/packages-apt.txt
RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
        && install_apt_packages /tmp/packages-apt.txt' \
    && rm -f /tmp/packages-apt.txt

# uv/uvx (Python package & venv manager), copied in rather than switching
# the base image — uv manages venvs against this image's existing system
# Python just as well as against a uv-provisioned one, without disturbing
# the apt/Node.js setup above.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/

# packages-uv.txt — general Python libraries (not CLI tools; see
# tools-uv.txt below), installed via `uv pip install --system` into this
# system Python. Runs as root, before the claude user exists, since it
# writes into the image's system site-packages rather than $HOME.
COPY packages-uv.txt /tmp/packages-uv.txt
RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
        && install_uv_packages /tmp/packages-uv.txt' \
    && rm -f /tmp/packages-uv.txt

RUN npm install -g @anthropic-ai/claude-code

# The claude account is created here, at a fixed placeholder UID/GID
# (1000:1000), purely so the root-only installers below (npm, uv tool,
# claude plugin) have a real non-root account to run as. `final` remaps
# this account to the operator's actual host UID/GID instead of recreating
# it, so a rebuild for a different host user reuses everything installed
# below unchanged rather than reinstalling it over the network.
RUN groupadd -g 1000 claude \
    && useradd -m -u 1000 -g 1000 -s /bin/bash claude \
    && mkdir -p /home/claude/.claude /workspace \
    && chown -R claude:claude /home/claude /workspace

# gosu switches UID/GID but, unlike a login shell, does not reset $HOME —
# without this, plugin install (below) and the runtime `claude` process would
# both try to use root's $HOME instead of /home/claude.
ENV HOME=/home/claude

# npm's default global-install prefix (/usr/local) sits on the read-only
# rootfs at runtime; repoint it to a location under the claude user's home
# so `npm install -g` works there instead. This doesn't affect the
# `npm install -g @anthropic-ai/claude-code` above, which already ran as
# root before this user/prefix existed. Set via ENV (not `npm config set`,
# which would only write ~/.npmrc into the image — that file wouldn't reach
# a volume that already has content the first time it's mounted here, since
# Docker's volume copy-up only seeds genuinely empty volumes) so the prefix
# applies regardless of what's already in the mounted $HOME volume.
ENV npm_config_prefix=/home/claude/.npm-global
# Home-managed installations intentionally precede profile-managed tools. The
# latter are image-owned under /opt so a populated home volume cannot hide a
# rebuilt profile image's requested versions.
ENV PATH="/home/claude/.local/bin:/home/claude/.npm-global/bin:/opt/agent-tools/bin:/opt/agent-tools/npm/bin:${PATH}"

# $HOME (uv's default cache location) and /workspace are deliberately
# separate mounts in this containment model, so uv's hardlink/reflink
# optimisation can never apply between them — declare the copy fallback
# explicit rather than have uv warn about it on every install.
ENV UV_LINK_MODE=copy

# packages-npm.txt / tools-uv.txt — see packages-apt.txt above. Profile-managed
# tools live outside the persistent home, so changing an image changes the
# visible managed version even when a populated home volume is mounted.
COPY packages-npm.txt /tmp/packages-npm.txt
COPY tools-uv.txt /tmp/tools-uv.txt
RUN mkdir -p /opt/agent-tools/npm /opt/agent-tools/uv-tools /opt/agent-tools/bin \
    && chown -R claude:claude /opt/agent-tools \
    && gosu claude env \
        npm_config_prefix=/opt/agent-tools/npm \
        UV_TOOL_DIR=/opt/agent-tools/uv-tools \
        UV_TOOL_BIN_DIR=/opt/agent-tools/bin \
        bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
        && install_npm_packages /tmp/packages-npm.txt \
        && install_uv_tools /tmp/tools-uv.txt' \
    && rm -f /tmp/packages-npm.txt /tmp/tools-uv.txt

COPY plugin-marketplaces.txt /tmp/plugin-marketplaces.txt
COPY plugins.txt /tmp/plugins.txt

# The official marketplace is always available; plugin-marketplaces.txt lists
# any additional (non-official) marketplaces to add before installing plugins,
# since plugins.txt entries may reference any of them via "<plugin>@<marketplace>".
RUN gosu claude claude plugin marketplace add anthropics/claude-plugins-official \
    && while IFS= read -r marketplace; do \
        case "$marketplace" in ''|'#'*) continue ;; esac; \
        gosu claude claude plugin marketplace add "${marketplace}"; \
    done < /tmp/plugin-marketplaces.txt \
    && while IFS= read -r plugin; do \
        case "$plugin" in ''|'#'*) continue ;; esac; \
        gosu claude claude plugin install "${plugin}" --scope user; \
    done < /tmp/plugins.txt \
    && rm /tmp/plugin-marketplaces.txt /tmp/plugins.txt

# Everything above this line is UID/GID-independent and layer-cacheable
# across hosts: the claude account exists at the fixed placeholder UID/GID
# only so the installers above have a non-root account to run as. From here
# down, `final` remaps that account to the operator's actual host UID/GID
# with --build-arg UID=$(id -u) --build-arg GID=$(id -g) — the base stage
# above is reused unchanged from cache, and nothing installed above is
# reinstalled, rather than rebuilding per host user.
FROM base AS final
ARG UID=1000
ARG GID=1000

# Only remap when the caller supplied a different UID/GID than the fixed
# placeholder baked into `base` above — usermod/groupmod plus a chown -R
# walk over already-installed content is far cheaper than rerunning the
# npm/uv-tool/plugin installs above, but it's skippable entirely for the
# common case where the caller never overrides UID/GID. Deliberately no -o:
# without it, usermod/groupmod refuse to reuse an ID already assigned to
# another account, so a caller can never remap claude onto UID/GID 0 (root)
# or collide it with another in-image identity — the build fails instead of
# silently succeeding.
RUN if [ "$UID" != "1000" ] || [ "$GID" != "1000" ]; then \
        groupmod -g "$GID" claude \
        && usermod -u "$UID" -g "$GID" claude \
        && chown -R "$UID":"$GID" /home/claude /workspace /opt/agent-tools; \
    fi

COPY --from=shared egress-allowlist.sh /usr/local/lib/agent/egress-allowlist.sh
COPY --from=shared workload-entrypoint.sh /usr/local/lib/agent/workload-entrypoint.sh

COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/lib/agent/workload-entrypoint.sh

# Strip setuid/setgid bits from the base image and every package installed
# above — required infra (install-workload-base.sh), packages-apt.txt,
# packages-npm.txt, packages-uv.txt, tools-uv.txt, and installed plugins —
# run last so nothing installed by any of those is missed. NoNewPrivs=1 (set
# by every documented run invocation) already makes these bits inert on
# execve, but that's an argument for stripping them, not against: if any
# single control ever regresses, a setuid-root `mount` becomes a live
# escalation primitive. None of these binaries are needed at runtime, so
# none should ship exploitable-shaped.
RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true

WORKDIR /workspace

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["claude"]
