# Autonomous RE agent container for Project Sylpheed. # # Builds and runs BOTH halves of the project — Xenia Canary (C++/CMake/Ninja) # as the behaviour oracle, and Sylpheed Reborn (Rust/Bevy) as the port — plus # the dynamic-RE toolkit that drives the emulator and reads its guest memory. # # Three things here exist because their absence cost the previous agent real # hours, and they are load-bearing rather than nice-to-have: # # 1. A REAL toolchain. The old box shipped runtime sonames only (libgtk-3.so.0 # but no libgtk-3.so), no cmake/ninja/clang and no libstdc++fs, so a full # build was impossible and `tools/re-capture/rebuild_canary.sh` had to # hand-relink object files. With -dev packages present that script is # obsolete; use `build-canary`. # 2. numpy and Pillow. Their absence silently disabled entities2.py, # flight_probe.py and every image oracle, and the failure looked like a # logic bug rather than a missing package. # 3. A display that outlives the turn. Xvfb kept dying "on its own every few # minutes"; it was being reaped because nothing owned it. Here it is a # child of PID 1 and lives exactly as long as the container. # # Clang is pinned to 19 to match the host that produced the checked-in build # caches (Ubuntu clang 19.1.1). FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive \ LANG=C.UTF-8 \ TZ=Etc/UTC # ── System packages ────────────────────────────────────────────────────────── RUN apt-get update && apt-get install -y --no-install-recommends \ # toolchain build-essential cmake ninja-build pkg-config git curl wget ca-certificates \ clang-19 lld-19 llvm-19 libc++-19-dev libc++abi-19-dev \ # Canary: GTK window, SDL input/audio, Vulkan, compression libgtk-3-dev libsdl2-dev liblz4-dev libvulkan-dev libx11-xcb-dev \ libxcb1-dev libxrandr-dev libssl-dev libfuse2t64 \ # Shader toolchain: the GPU build shells out to `glslangValidator` and the # SPIR-V tools to compile xenia's own shaders. Missing them does not fail # configure — it fails ~500 objects in, as a Python FileNotFoundError. glslang-tools spirv-tools spirv-headers \ # Vulkan runtime — lavapipe (software) plus the real ICDs for /dev/dri mesa-vulkan-drivers vulkan-tools libvulkan1 libgl1-mesa-dri libglx-mesa0 \ # Reborn / Bevy: audio, input, windowing libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev \ libx11-dev libxi-dev libxcursor-dev libxinerama-dev libxext-dev \ # headless display + window manager + the screenshot path xvfb x11-utils x11-xserver-utils openbox xdotool imagemagick ffmpeg \ # dynamic RE python3 python3-numpy python3-pil python3-pip \ gdb strace ltrace binutils file xxd ripgrep jq unzip zip p7zip-full \ procps psmisc lsof less nano tini sudo \ # expect drives Claude Code's one-time interactive gates for an # unattended run — see bin/claude-autonomous. expect \ && rm -rf /var/lib/apt/lists/* # Pin the unversioned tool names to 19 so CMake, and anything that shells out to # `clang`, agree with what the caches were built by. RUN for t in clang clang++ lld ld.lld llvm-ar llvm-ranlib llvm-nm clang-cpp; do \ src="/usr/bin/${t}-19"; \ [ -e "$src" ] && update-alternatives --install "/usr/bin/${t}" "$t" "$src" 200 || true; \ done # duckdb reads the static-analysis database (sylpheed.db); it is not packaged. # PEP 668 marks the system env externally-managed, and this image has no other # Python consumer to protect, so installing into it is the honest simple option. RUN pip3 install --no-cache-dir --break-system-packages duckdb # ── Node + Claude Code ─────────────────────────────────────────────────────── RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && npm install -g @anthropic-ai/claude-code \ && npm cache clean --force \ && rm -rf /var/lib/apt/lists/* # ── The agent user ─────────────────────────────────────────────────────────── # NOT root, and not negotiable: Claude Code refuses --dangerously-skip-permissions # when it has root privileges. uid/gid 1000 matches the host account so files # written into the bind-mounted repos keep the right ownership. ARG AGENT_UID=1000 ARG AGENT_GID=1000 # Ubuntu 24.04 ships its own `ubuntu` account at uid/gid 1000, so the common # case — matching a host user who is also 1000 — collides with it. Remove the # stock account first; nothing in this image uses it. RUN if getent passwd "${AGENT_UID}" >/dev/null; then \ userdel -r "$(getent passwd "${AGENT_UID}" | cut -d: -f1)" 2>/dev/null || true; \ fi; \ if getent group "${AGENT_GID}" >/dev/null; then \ groupdel "$(getent group "${AGENT_GID}" | cut -d: -f1)" 2>/dev/null || true; \ fi; \ groupadd -g "${AGENT_GID}" agent \ && useradd -m -u "${AGENT_UID}" -g "${AGENT_GID}" -s /bin/bash -d /sylph-home/re agent \ && mkdir -p /sylph-home/re /work \ && chown -R "${AGENT_UID}:${AGENT_GID}" /sylph-home \ && echo 'agent ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/agent COPY bin/ /usr/local/bin/ COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/* /usr/local/bin/entrypoint.sh USER agent WORKDIR /work # ── Rust ───────────────────────────────────────────────────────────────────── # CARGO_TARGET_DIR deliberately points OUTSIDE the bind-mounted repo: the host # also builds Reborn, and sharing target/ makes the two invalidate each other's # incremental state on every switch. ENV RUSTUP_HOME=/sylph-home/re/.rustup \ CARGO_HOME=/sylph-home/re/.cargo \ CARGO_TARGET_DIR=/sylph-home/re/target-container \ PATH=/sylph-home/re/.cargo/bin:/usr/local/bin:/usr/bin:/bin RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --default-toolchain stable --profile minimal \ --component clippy --component rustfmt \ && rustup target add wasm32-unknown-unknown # trunk serves the Reborn viewer's wasm build; the release binary avoids a # ten-minute `cargo install`. RUN curl -fsSL https://github.com/trunk-rs/trunk/releases/download/v0.21.4/trunk-x86_64-unknown-linux-gnu.tar.gz \ | tar -xz -C /sylph-home/re/.cargo/bin trunk # Create the volume mount points HERE, owned by `agent`. Docker seeds an empty # named volume from whatever the image has at that path — including ownership — # but if the path does not exist it creates a root-owned directory instead, and # the first write fails with something as unhelpful as # "CMake Error: Unable to (re)create the private pkgRedirects directory". RUN mkdir -p /sylph-home/re/target-container /sylph-home/re/canary-build /sylph-home/re/.claude # ── Runtime environment ────────────────────────────────────────────────────── # DISPLAY :98 and HOME /sylph-home/re are what tools/re-capture/*.sh already # assume; keeping them means the existing toolkit runs unmodified. ENV HOME=/sylph-home/re \ DISPLAY=:98 \ SCREEN_GEOMETRY=1280x720x24 \ PROJECT_DIR=/work \ XENIA_PAD_FILE=/tmp/xenia_pad.txt \ XENIA_BUILD_DIR=/sylph-home/re/canary-build \ SDL_AUDIODRIVER=dummy \ LIBGL_ALWAYS_SOFTWARE=1 \ PATH=/work/Syplheed-Reborn/tools/re-capture/bin:/work/Syplheed-Reborn/tools/re-capture:/sylph-home/re/.cargo/bin:/usr/local/bin:/usr/bin:/bin ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] CMD ["bash"]