# loopat sandbox base image.
#
# The base is a "full-stack" interactive workbench: the user can apt
# install, git, make, curl, etc. inside the loop without leaving. Per-loop
# child images bake mise-managed toolchains on top of this base (see
# `ensureLoopImage` in server/src/podman.ts).
#
# Built once at first launch (or via scripts/build-sandbox-image.sh) and
# tagged `loopat-sandbox:latest`. Each loop's container is `podman
# create`'d from this image — or, when the loop has a composed mise.toml,
# from a per-loop child `loopat-sandbox-<hash>:latest`.
#
# Base: Ubuntu 24.04 so glibc matches the dev hosts (we use Ubuntu 24.04).
# The host's claude binary binds in via --volume and runs against this glibc.
FROM ac2-registry.cn-hangzhou.cr.aliyuncs.com/ac2/base:ubuntu24.04

ARG DEBIAN_FRONTEND=noninteractive

USER root

# Strip the image's default `ubuntu` user (uid 1000) and create our own
# fixed `loopat` user at uid 2000. The container then ALWAYS appears as
# `loopat` to whoever's inside, regardless of which host user is running
# rootless podman — see `--userns=keep-id:uid=2000,gid=2000` in podman.ts.
# Why uid 2000: avoid colliding with 1000 (still common host uid; the
# original ubuntu user previously claimed it inside this image).
#
# Why not root: the claude binary refuses --dangerously-skip-permissions
# when uid == 0 ("for security reasons"). The SDK driver always uses
# bypassPermissions in loopat, so container-root is untenable.
#
# Full-stack feel: sudo NOPASSWD for loopat → `sudo apt install <pkg>`
# from any shell, no prompts.
RUN userdel -r ubuntu 2>/dev/null || true \
 && groupadd -g 2000 loopat \
 && useradd -m -u 2000 -g 2000 -s /bin/bash loopat

# The AC2 base ships apt sources pointing at mirrors.cloud.aliyuncs.com
# (100.100.x.x — Aliyun *intranet* only, unreachable outside ECS). Repoint to
# the public mirrors.aliyun.com; the per-arch path (ubuntu / ubuntu-ports) is
# preserved, so this works on both amd64 and arm64.
RUN sed -i 's|mirrors.cloud.aliyuncs.com|mirrors.aliyun.com|g' \
        /etc/apt/sources.list.d/*.sources /etc/apt/sources.list 2>/dev/null || true \
 && printf 'Acquire::Retries "5";\n' > /etc/apt/apt.conf.d/80-loopat-retries

# Full-stack system layer. Everything a dev reflexively reaches for.
# podman/uidmap/fuse-overlayfs/slirp4netns: nested rootless podman support
# — every sandbox can run podman without per-loop opt-in. The sandbox's
# create-args carry `--privileged --device /dev/fuse`, and inner loopat
# (or any AI workflow) probes for podman at runtime via the existing
# self-detection path.
RUN apt-get update \
 && apt-get install -y --no-install-recommends \
        bash coreutils findutils grep sed less \
        util-linux procps bsdmainutils ca-certificates \
        sudo curl git build-essential openssh-client \
        jq vim fish python3-minimal \
        podman uidmap fuse-overlayfs slirp4netns \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

# Passwordless sudo for the loopat user.
RUN echo "loopat ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/loopat \
 && chmod 0440 /etc/sudoers.d/loopat

# Nested rootless podman setup for `loopat` (uid 2000).
#
# Why the unusual subuid range (10000:50000, not the typical 100000:65536):
# the OUTER container's userns size is bounded by the host user's subuid
# allocation (typically 65536). With `--userns=keep-id:uid=2000,gid=2000`,
# uids 0..65535 are mapped inside; uid 100000 simply doesn't exist in
# this namespace, so newuidmap rejects it. Inner subuid must be a SUBSET
# of the outer-mapped range. 10000:50000 stays within 0..65535 and avoids
# clashing with uid 2000 (loopat itself).
#
# Storage driver `vfs`: rootless-in-rootless overlayfs is fragile across
# kernel/podman/storage combinations. vfs always works (cost: disk space
# proportional to layers × containers). Switch to fuse-overlayfs later if
# disk pressure shows up.
#
# Cgroup manager `cgroupfs`: there's no systemd inside the sandbox, so
# the systemd cgroup driver can't be used.
# NOTE on `>`: Ubuntu's useradd auto-allocates `loopat:100000:65536` via
# /etc/login.defs defaults. Appending our entry leaves both, and rootless
# podman tries to apply BOTH ranges — the 100000 one falls outside outer's
# 0..65535 view and newuidmap fails. Overwrite, don't append.
RUN echo "loopat:10000:50000" > /etc/subuid \
 && echo "loopat:10000:50000" > /etc/subgid \
 && mkdir -p /home/loopat/.config/containers \
 && printf '[storage]\ndriver = "vfs"\n' > /home/loopat/.config/containers/storage.conf \
 && printf '[containers]\ncgroup_manager = "cgroupfs"\n' > /home/loopat/.config/containers/containers.conf \
 && printf 'unqualified-search-registries = ["docker.io"]\n' > /home/loopat/.config/containers/registries.conf \
 && chown -R loopat:loopat /home/loopat/.config

# mise — tool version manager. Per-loop child images bake their tools via
# `mise install` during their build, so the toolchain ends up baked into
# image layers and reused across loops with the same composed mise.toml.
# Note: mise.run's installer reads MISE_INSTALL_PATH (full path to the
# binary), NOT a "dir" env var.
RUN curl -fsSL https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh

# mise state lives OUTSIDE $HOME so the per-loop $HOME overlay (used for
# persistent shell history) can't shadow the tools the image bakes in.
# Shims dir on PATH makes every tool globally callable without needing
# `mise activate` in each shell.
ENV MISE_DATA_DIR=/opt/loopat-mise \
    MISE_CONFIG_DIR=/opt/loopat-mise/config \
    MISE_CACHE_DIR=/opt/loopat-mise/cache \
    PATH=/opt/loopat-mise/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

RUN mkdir -p /opt/loopat-mise/shims /opt/loopat-mise/config /opt/loopat-mise/cache \
 && chown -R loopat:loopat /opt/loopat-mise \
 && chmod -R 755 /opt/loopat-mise

# Native host-cli forwarder. The sandbox's built-in way to run a host-only cli
# (macOS-only, or a machine-bound company cli) on behalf of the loop:
#   shim(<cli>) → loopat-host → mounted unix socket → host execFile
# This forwarder is generic (same for every loop), so it's baked into the image.
# Per-cli shims are generated per-loop from the loop's mise.toml `[host].clis`
# and COPY'd into the mise shims dir (already first on PATH) by the per-loop
# child build (ensureLoopImage). The socket dir is mounted in and the
# LOOPAT_HOST_SOCK / LOOPAT_LOOP_ID env vars are injected at container create.
COPY loopat-host /usr/local/bin/loopat-host
RUN chmod 0755 /usr/local/bin/loopat-host

# Switch to loopat. mise install (per-loop child build) and runtime
# exec'd processes all run as this user.
USER loopat

# The container's main process is just a long-lived sleeper holding the
# namespaces open. SDK driver + PTY shell `podman exec` in as siblings.
CMD ["/bin/sleep", "infinity"]
