# Cloud Computer image: the production browser worker plus a usable desktop.
#
# Plan: common-docs/projects/cloud-computer/PLAN.md (P1a). Build context is the
# monorepo root, like the browser worker:
#
#   docker buildx build --platform linux/amd64 \
#     -f packages/matrx-scraper/docker/cloud-computer/Dockerfile \
#     --build-arg BROWSER_WORKER_IMAGE=<ecr>/matrx-browser-worker:<sha> \
#     -t <ecr>/matrx-cloud-computer:<sha> .
#
# BASE: an exact, already-built browser-worker image (Chrome, Xvfb, Selkies,
# fonts, the uv environment). It is never rebuilt here, so the heavy layers stay
# byte-identical to the fleet's. On top: the smallest desktop that a person
# taking control can use without help (Openbox, a pcmanfm desktop with a
# wallpaper and Chrome / Terminal / Files icons, a tint2 taskbar), sudo so agents can install software on
# the writable root filesystem, scrot/xdotool for whole-desktop screenshots and
# input, and the CURRENT worker source so a machine never lags the repo.
ARG BROWSER_WORKER_IMAGE
FROM ${BROWSER_WORKER_IMAGE}

USER root

RUN apt-get update && apt-get install -y --no-install-recommends \
        openbox tint2 lxterminal pcmanfm adwaita-icon-theme librsvg2-common \
        sudo scrot xdotool x11-xserver-utils dbus-x11 numlockx rsync \
    && rm -rf /var/lib/apt/lists/* /usr/share/doc/* /usr/share/man/* \
    && echo 'browser-worker ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-cloud-computer \
    && chmod 0440 /etc/sudoers.d/90-cloud-computer \
    && visudo -cf /etc/sudoers.d/90-cloud-computer \
    && command -v openbox tint2 lxterminal pcmanfm scrot xdotool numlockx rsync \
    && install -d -m 0755 /etc/xdg/cloud-computer /etc/xdg/pcmanfm/cloud-computer /usr/local/share/cloud-computer \
    && sed '/<applications>/a <application class="matrx-agent-chrome" type="normal"><maximized>yes</maximized><focus>yes</focus></application>' \
         /etc/xdg/openbox/rc.xml > /etc/xdg/cloud-computer/openbox-rc.xml \
    && grep -q 'class="matrx-agent-chrome"' /etc/xdg/cloud-computer/openbox-rc.xml \
    && sed -i -e '/<desktops>/,/<\/desktops>/{s#<number>[0-9]*</number>#<number>1</number>#;s#<popupTime>[0-9]*</popupTime>#<popupTime>0</popupTime>#}' \
         /etc/xdg/cloud-computer/openbox-rc.xml \
    && sed -n '/<desktops>/,/<\/desktops>/p' /etc/xdg/cloud-computer/openbox-rc.xml | grep -q '<number>1</number>' \
    && sed -n '/<desktops>/,/<\/desktops>/p' /etc/xdg/cloud-computer/openbox-rc.xml | grep -q '<popupTime>0</popupTime>' \
    && chmod 0644 /etc/xdg/cloud-computer/openbox-rc.xml \
    && install -d -m 0755 /etc/xdg/libfm && touch /etc/xdg/libfm/libfm.conf \
    && { grep -q '^\[config\]' /etc/xdg/libfm/libfm.conf || printf '[config]\n' >> /etc/xdg/libfm/libfm.conf; } \
    && if grep -q '^quick_exec=' /etc/xdg/libfm/libfm.conf; then \
         sed -i 's/^quick_exec=.*/quick_exec=1/' /etc/xdg/libfm/libfm.conf; \
       else sed -i '/^\[config\]/a quick_exec=1' /etc/xdg/libfm/libfm.conf; fi \
    && grep -q '^quick_exec=1' /etc/xdg/libfm/libfm.conf

# Agent shell/fs tools (PLAN P2): the SAME sandbox daemon every Matrx Sandbox
# runs (matrx_agent, matrx-sandbox repo, public), pinned to one commit and its
# archive hash, in its own venv so it never touches the worker's environment.
# It listens on loopback only; the worker's /agent door verifies the platform
# token and forwards (matrx_scraper/cloud_browser/worker/machine_agent.py).
ARG MATRX_SDK_COMMIT=d72d9ca891fb8e9fa3d2dbeb04eaf37cdb758f9f
ARG MATRX_SDK_SHA256=e9e767778ddae719c51a036eff4f3681916350f47a00b99c818817ffa26e2e2d
RUN set -eu; tmp="$(mktemp -d)"; \
    /app/.venv/bin/python -c "import urllib.request,sys; urllib.request.urlretrieve(sys.argv[1], sys.argv[2])" \
      "https://codeload.github.com/armanisadeghi/matrx-sandbox/tar.gz/${MATRX_SDK_COMMIT}" "${tmp}/sdk.tgz"; \
    echo "${MATRX_SDK_SHA256}  ${tmp}/sdk.tgz" | sha256sum -c -; \
    tar xzf "${tmp}/sdk.tgz" -C "${tmp}"; \
    uv venv --python /app/.venv/bin/python /opt/matrx-agent; \
    uv pip install --no-cache --python /opt/matrx-agent/bin/python "${tmp}/matrx-sandbox-${MATRX_SDK_COMMIT}/sandbox-image/sdk"; \
    rm -rf "${tmp}"; chmod -R a+rX /opt/matrx-agent; \
    cd /tmp && HOT_PATH=/tmp MATRX_WORKSPACE_ROOT=/tmp /opt/matrx-agent/bin/python -c "import matrx_agent.api.main, uvicorn"

# Current worker source over the base image's editable install, plus the
# browser-worker entrypoint that carries the desktop-session hook. Modes are
# set by COPY --chmod so no later RUN rewrites (and so duplicates) a layer.
COPY --chmod=a+rX packages/matrx-scraper/matrx_scraper /app/packages/matrx-scraper/matrx_scraper
COPY --chmod=0755 packages/matrx-scraper/docker/browser-worker-entrypoint.sh /usr/local/bin/browser-worker-entrypoint
COPY --chmod=0755 packages/matrx-scraper/docker/cloud-computer/cloud-computer-entrypoint.sh /usr/local/bin/cloud-computer-entrypoint
COPY --chmod=0755 packages/matrx-scraper/docker/cloud-computer/cloud-computer-desktop.sh /usr/local/bin/cloud-computer-desktop
COPY --chmod=0755 packages/matrx-scraper/docker/cloud-computer/cloud-computer-chrome.sh /usr/local/bin/cloud-computer-chrome
COPY --chmod=0755 packages/matrx-scraper/docker/cloud-computer/cloud-computer-software.sh /usr/local/bin/cloud-computer-software
COPY --chmod=0755 packages/matrx-scraper/docker/cloud-computer/cloud-computer-carryover.py /usr/local/bin/cloud-computer-carryover
COPY --chmod=0755 packages/matrx-scraper/docker/cloud-computer/cloud-files.sh /usr/local/bin/cloud-files
# The parent directories already exist with 0755 (above): a COPY --chmod into
# a missing directory creates it with the FILE's mode, and a 0644 directory
# (no execute bit) left tint2 unable to read its config (2026-10-17).
COPY --chmod=0644 packages/matrx-scraper/docker/cloud-computer/tint2rc /etc/xdg/cloud-computer/tint2rc
COPY --chmod=0644 packages/matrx-scraper/docker/cloud-computer/desktop-items-0.conf /etc/xdg/pcmanfm/cloud-computer/desktop-items-0.conf
COPY --chmod=0644 packages/matrx-scraper/docker/cloud-computer/make-wallpaper.py /usr/local/share/cloud-computer/make-wallpaper.py
COPY --chmod=0644 packages/matrx-scraper/docker/cloud-computer/cloud-computer-chrome.desktop /usr/share/applications/cloud-computer-chrome.desktop
COPY --chmod=0644 packages/matrx-scraper/docker/cloud-computer/cloud-computer-terminal.desktop /usr/share/applications/cloud-computer-terminal.desktop
COPY --chmod=0644 packages/matrx-scraper/docker/cloud-computer/cloud-computer-files.desktop /usr/share/applications/cloud-computer-files.desktop
# Wallpaper, rendered once at build; then prove every desktop config is readable
# by the unprivileged desktop user (the defect class behind the missing taskbar).
RUN /app/.venv/bin/python /usr/local/share/cloud-computer/make-wallpaper.py /usr/local/share/cloud-computer/wallpaper.png 1920 1200 \
    && chmod 0644 /usr/local/share/cloud-computer/wallpaper.png \
    && for f in /etc/xdg/cloud-computer/tint2rc /etc/xdg/cloud-computer/openbox-rc.xml \
                /etc/xdg/pcmanfm/cloud-computer/desktop-items-0.conf /usr/local/share/cloud-computer/wallpaper.png \
                /usr/share/applications/cloud-computer-chrome.desktop; do \
         runuser --user browser-worker -- test -r "$f" || { echo "unreadable by the desktop user: $f" >&2; exit 1; }; \
       done
# Prove the entrypoint's EXACT Xvfb line starts a display (a flag Xvfb does not
# know, "-nodpms", made it exit at once and machines boot only by luck, 2026-10-18).
RUN line="$(grep -E '^Xvfb ' /usr/local/bin/browser-worker-entrypoint)" && test -n "$line" \
    && worker_display=:57 worker_width=64 worker_height=64 \
    && eval "${line% &} &" && pid=$! \
    && for _ in $(seq 1 50); do xdpyinfo -display :57 >/dev/null 2>&1 && break; sleep 0.1; done \
    && xdpyinfo -display :57 >/dev/null && kill "$pid"
# The screen page: Selkies' web client with no stock interface, our loader and
# the postMessage contract the app embeds against (stream-client/), plus the
# Selkies app's non-blocking wait for the viewer's peer and its TURN list
# replaced (never appended) on each RTC refresh. The patch checks the
# exact upstream bytes first and fails the build on any drift.
COPY --chmod=a+rX packages/matrx-scraper/docker/cloud-computer/stream-client/ /usr/local/share/cloud-computer/stream-client/
COPY --chmod=0644 packages/matrx-scraper/docker/cloud-computer/patch-selkies-web.py /usr/local/share/cloud-computer/patch-selkies-web.py
RUN /app/.venv/bin/python /usr/local/share/cloud-computer/patch-selkies-web.py \
        /opt/selkies-gstreamer/share/selkies-web /usr/local/share/cloud-computer/stream-client \
        /opt/selkies-gstreamer/lib/python3.12/site-packages/selkies_gstreamer \
    && grep -q 'matrx-stream.js' /opt/selkies-gstreamer/share/selkies-web/index.html \
    && grep -q '_matrx_wait_for_peer' /opt/selkies-gstreamer/lib/python3.12/site-packages/selkies_gstreamer/__main__.py \
    && grep -q '_matrx_app.turn_servers' /opt/selkies-gstreamer/lib/python3.12/site-packages/selkies_gstreamer/__main__.py

# Bytecode for the copied source: PYTHONDONTWRITEBYTECODE=1 means a module whose
# .pyc is missing or stale is recompiled in memory on EVERY boot.
RUN cd /app && uv run --no-sync python -m compileall -q /app/packages/matrx-scraper/matrx_scraper
# Build-time proof the copied source imports.
RUN cd /app && UV_NO_CACHE=1 uv run --no-sync python -c "import matrx_scraper.cloud_browser.worker.server" 2>&1 | grep -q "BROWSER_WORKER_ID is required"

# Boot read-ahead list. The image is lazily loaded (SOCI): every file is fetched
# from ECR the first time it is read, so the worker's ~650 import files and the
# display binaries cost seconds when read one by one at boot. The entrypoint
# reads this list in parallel the moment the container starts (measured
# 2026-10-16: warm import 1.8 s vs 7.4 s at boot).
RUN set -eu; list=/usr/local/share/cloud-computer/boot-files; mkdir -p /usr/local/share/cloud-computer; \
    cd /app && UV_NO_CACHE=1 uv run --no-sync python -v -c "import uvicorn.main, matrx_scraper.cloud_browser.worker.server" 2>&1 \
      | sed -n -e "s/^# code object from '\(.*\)'$/\1/p" -e "s/^# extension module '.*' loaded from '\(.*\)'$/\1/p" > /tmp/boot-files; \
    test "$(wc -l < /tmp/boot-files)" -gt 300; \
    bins="$(command -v Xvfb xdpyinfo openbox tint2 xprop xsetroot) /app/.venv/bin/python"; \
    { cat /tmp/boot-files; for b in $bins /app/.venv/bin/uvicorn; do readlink -f "$b"; done; \
      ldd $bins 2>/dev/null | awk '$3 ~ /^\// {print $3}'; } | sort -u > "$list"; \
    rm -f /tmp/boot-files; wc -l "$list"

# Second read-ahead list: what the person's FIRST picture waits on after the
# worker answers. Chrome's launch inside /bootstrap took 8.7-9.2 s on a
# just-woken machine vs 2.5-3 s on a warm one (FIX-OPEN-LATENCY / FIX-COLD-OPEN
# 2026-10-18): every Chrome, Playwright-driver and stream-page file is fetched
# from ECR on first read. The entrypoint reads this list right after boot-files,
# while the worker and desktop start (~6 s before the worker first answers).
RUN set -eu; list=/usr/local/share/cloud-computer/open-files; \
    driver="$(cd /app && UV_NO_CACHE=1 uv run --no-sync python -c 'import os, playwright; print(os.path.join(os.path.dirname(playwright.__file__), "driver"))')"; \
    { find /opt/google/chrome "$driver" /opt/selkies-gstreamer/share/selkies-web -type f; \
      ldd /opt/google/chrome/chrome 2>/dev/null | awk '$3 ~ /^\// {print $3}'; } | sort -u > "$list"; \
    test "$(wc -l < "$list")" -gt 100; wc -l "$list"

# The browser-worker entrypoint starts this after Xvfb answers. The stream
# offers clipboard sync on a person's machine (off in the browser fleet).
ENV BROWSER_WORKER_DESKTOP_SESSION=/usr/local/bin/cloud-computer-desktop \
    BROWSER_WORKER_STREAM_CLIPBOARD=1 \
    BROWSER_WORKER_STREAM_KEEP_WARM=1 \
    MATRX_CLOUD_COMPUTER=1

# Upgrades keep everything (owner ruling 16): every apt/dpkg run records the
# person's system installs into their home; the entrypoint replays them at wake.
# The image's own package and file lists are written LAST, so nothing this
# build installs is ever mistaken for the person's.
ARG CLOUD_COMPUTER_BUILD=unknown
RUN printf '%s\n' "${CLOUD_COMPUTER_BUILD}" > /usr/local/share/cloud-computer/build \
    && printf '%s\n' 'DPkg::Post-Invoke { "/usr/local/bin/cloud-computer-software record >/dev/null 2>&1 || true"; };' \
        > /etc/apt/apt.conf.d/90-cloud-computer-record \
    && apt-mark showmanual | sort -u > /usr/local/share/cloud-computer/image-packages.txt \
    && find /etc/apt /usr/share/keyrings /usr/local /opt -xdev \( -type f -o -type l \) 2>/dev/null | sort -u \
         > /usr/local/share/cloud-computer/image-files.txt \
    && test -s /usr/local/share/cloud-computer/image-packages.txt \
    && grep -qx /usr/local/bin/cloud-computer-software /usr/local/share/cloud-computer/image-files.txt \
    && chmod 0644 /usr/local/share/cloud-computer/image-packages.txt /usr/local/share/cloud-computer/image-files.txt /usr/local/share/cloud-computer/build

ENTRYPOINT ["/usr/local/bin/cloud-computer-entrypoint"]
CMD []
