Phase 5 of docs/agents/GITEA-SETUP.md, plus a correction to Phase 2 that the runbook could not have known it needed. gitea-mcp v1.7.0 goes into both images, pinned by the sha256 the release publishes and smoke-tested with `--version` at build time, so a bad pin fails the build instead of the agent. Each entrypoint registers it at user scope for that container's own identity, remove-then-add so a restart is idempotent. The token is passed BY PATH. `-e GITEA_ACCESS_TOKEN=$(cat …)` would write it in cleartext into ~/.claude.json, which every session in the container reads; GITEA_ACCESS_TOKEN_FILE is new in the pinned version and leaves the secret in its read-only mount. Verified against the binary's own --help, not assumed. The tool filter stops being an experiment. The names are in the release README: each agent gets issues, notifications, labels, milestones and pull requests, and NOT `pull_request_review_write`. That one matters because separate identities open a hole the runbook did not name: Gitea refuses to let an author approve their own pull request, and does nothing about sylph-decoder approving sylph-port's. Two agents could satisfy `required_approvals = 1` between themselves and then merge, since branch protection blocks pushes to main and never blocked merges. Withholding the tool is defence in depth; the controls are in branch protection, and both docs now say so: approvals whitelisted to the human so an agent's approval does not count, merges whitelisted to the human so an approved PR is still merged by a person. Phase 2's check gains the step that actually tests it -- approve the throwaway PR yourself, then confirm the agent STILL has no merge button. Without that step, the check passes on an instance where the agents can merge each other's work. Also settles two entries on the runbook's own "not verified" list: the tool filter names, and the Gitea version (1.25.5, whose API schema carries enable_merge_whitelist and enable_approvals_whitelist under those names). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
171 lines
9.5 KiB
Docker
171 lines
9.5 KiB
Docker
# 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 \
|
|
# PulseAudio, for capturing audio without a sound card. `module-null-sink`
|
|
# is a real device as far as any application is concerned, so the emulator
|
|
# and Godot open it normally and `parec` records what they play. Without
|
|
# it, "does this actually sound right" is unanswerable in a container --
|
|
# and the cue-to-event bindings stay a name match rather than a
|
|
# measurement. See docs/port/AUDIO-VERIFICATION.md.
|
|
pulseaudio pulseaudio-utils \
|
|
&& 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/*
|
|
|
|
# ── gitea-mcp ────────────────────────────────────────────────────────────────
|
|
# The agent's hands on issues, pull requests and notifications — Gitea's own MCP
|
|
# server, so there is no second store of truth to drift out of sync with the
|
|
# first.
|
|
#
|
|
# PINNED AND CHECKSUMMED, not "whatever is at that URL today": this binary is
|
|
# handed a token that can write to the repository. The checksum is the one
|
|
# published in `gitea-mcp_1.7.0_checksums.txt` for the Linux x86_64 asset.
|
|
ARG GITEA_MCP_VERSION=1.7.0
|
|
ARG GITEA_MCP_SHA256=bbc9a7b462facd3c56b1558ee6054e91f2fca27a2878b5599afddcf57d446b8d
|
|
RUN curl -fsSL -o /tmp/gitea-mcp.tar.gz \
|
|
"https://gitea.com/gitea/gitea-mcp/releases/download/v${GITEA_MCP_VERSION}/gitea-mcp_Linux_x86_64.tar.gz" \
|
|
&& echo "${GITEA_MCP_SHA256} /tmp/gitea-mcp.tar.gz" | sha256sum -c - \
|
|
&& tar -xzf /tmp/gitea-mcp.tar.gz -C /usr/local/bin gitea-mcp \
|
|
&& chmod +x /usr/local/bin/gitea-mcp \
|
|
&& rm -f /tmp/gitea-mcp.tar.gz \
|
|
&& gitea-mcp --version
|
|
|
|
# ── 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 /exchange \
|
|
&& chown -R "${AGENT_UID}:${AGENT_GID}" /sylph-home /work /exchange \
|
|
&& 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/tools:/work/tools/re-capture/bin:/work/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"]
|