# Autonomous port agent for the Sylpheed Godot menu shell. # # DELIBERATELY SMALL. The reverse-engineering container next door is 4.36 GB # because it builds Xenia Canary and drives it under a software Vulkan stack. # This agent has no emulator, no oracle and no C++ build: it converts already- # decoded assets and drives Godot. Keeping it light is what lets both containers # run on one 12-core / 15 GB box without the memory pressure that has crashed it. # # What it needs, and nothing else: Rust (the exporter), Godot 4 (the runtime), # ffmpeg (the transcode), and a headless display to screenshot Godot for # comparison against the reference renderer. FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive \ LANG=C.UTF-8 \ TZ=Etc/UTC RUN apt-get update && apt-get install -y --no-install-recommends \ # toolchain for the exporter and for building sylpheed-cli from /reborn build-essential pkg-config git curl ca-certificates \ libssl-dev \ # Godot 4 needs these even headless; the windowed run needs the X libs libx11-6 libxcursor1 libxinerama1 libxrandr2 libxi6 libgl1 \ libasound2t64 libpulse0 libfontconfig1 \ # the transcode target (libtheora + libvorbis ship in Ubuntu's ffmpeg) ffmpeg \ # headless display + the screenshot path, for diffing Godot's output # against `sylpheed-cli screen render` xvfb x11-utils openbox imagemagick \ # everyday python3 jq ripgrep unzip file less nano tini sudo procps \ # expect drives Claude Code's one-time interactive gates 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/* # ── Godot 4 ────────────────────────────────────────────────────────────────── # Pinned. An engine version bump changes rendering, and this project compares # screenshots against a reference renderer — so an upgrade must be a deliberate, # stated act rather than a silent drift. ARG GODOT_VERSION=4.7.2 RUN cd /tmp \ && curl -fsSLO "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip" \ && unzip -q "Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip" \ && mv "Godot_v${GODOT_VERSION}-stable_linux.x86_64" /usr/local/bin/godot \ && chmod +x /usr/local/bin/godot \ && printf '#!/bin/sh\nexec /usr/local/bin/godot --headless "$@"\n' > /usr/local/bin/godot-headless \ && chmod +x /usr/local/bin/godot-headless \ && rm -f "Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip" # ── 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: Claude Code refuses --dangerously-skip-permissions with root # privileges. Ubuntu 24.04 ships its own `ubuntu` account at uid 1000, so the # common case — matching a host user who is also 1000 — collides with it. ARG AGENT_UID=1000 ARG AGENT_GID=1000 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/port agent \ && mkdir -p /sylph-home/port /work /exchange /reborn \ && 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 # CARGO_TARGET_DIR points OUTSIDE the bind-mounted repo so the host and the # container do not invalidate each other's incremental state on every switch. ENV RUSTUP_HOME=/sylph-home/port/.rustup \ CARGO_HOME=/sylph-home/port/.cargo \ CARGO_TARGET_DIR=/sylph-home/port/target-container \ PATH=/work/tools:/sylph-home/port/.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 RUN mkdir -p /sylph-home/port/target-container /sylph-home/port/.claude ENV HOME=/sylph-home/port \ DISPLAY=:97 \ SCREEN_GEOMETRY=1280x720x24 \ PROJECT_DIR=/work ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] CMD ["bash"]