This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Sylpheed-Godot/docker/sylph-port
MechaCat02 0fccea389d port: fix the credential mount, the /reborn pull, and cargo re-fetching
Three fixes from the port agent's first infrastructure report.

**A. The credential file is written, so it cannot be a read-only mount.**
`credential.helper=store` rewrites its file after a successful auth: temp file,
then rename over the target. Renaming onto a bind-mount point gives EBUSY, which
surfaces as `fatal: unable to write credential store: Device or resource busy`.

The push succeeds anyway, and that is the real hazard -- a `fatal:` line that is
routinely wrong teaches the reader to ignore the one that is real. It also fired
intermittently, so it read as flakiness rather than as a mount.

Fixed by mirroring the pattern already used for .claude.json: mount it as
`.git-credentials.host:ro` and have the entrypoint copy it to a writable
~/.git-credentials at 600. Mounting rw would also silence it, but then the
container can clobber the host's real credential file; copying cannot.

**B. `git -C /reborn pull` can never work, and should not.** /reborn is a live
read-only mount of the RE agent's working tree -- it updates itself, and pulling
would move another agent's checkout. The prompt now says so, and adds the
consequence the agent found the hard way: because the mount is live, HANDOFF can
move mid-iteration, so anything copied out of it (BLOCKED.md especially) may
already be stale and must be re-checked rather than trusted.

**C.** CARGO_HOME moves to a named volume; it was on the container overlay, so
the pinned decoder source was re-fetched from the network on every fresh start.

Also adds SYLPH_PORT_REPO, so this launcher can be run from a worktree without
repointing the agent's checkout -- which is how these edits were made, the agent
being mid-iteration on auto/p0-exporter in the shared tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 21:07:13 +02:00

181 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Launcher for the Godot port agent.
#
# ./sylph-port build build the image
# ./sylph-port shell interactive shell
# ./sylph-port loose [task] detached, self-running on a fixed interval
# ./sylph-port logs -f follow it
# ./sylph-port attach chat with it (Ctrl-P Ctrl-Q to leave it running)
# ./sylph-port remote a link to chat with it from anywhere
# ./sylph-port stop stop it
#
# Env:
# SYLPH_PORT_CPUS / SYLPH_PORT_MEM_GB override the cap (default 3 / 4)
# SYLPH_PORT_REPO repo to mount at /work (default: this script's parent)
# SYLPH_REBORN path to the Syplheed-Reborn checkout (read-only mount)
# SYLPH_DISC extracted disc root
# SYLPH_GIT_CREDENTIALS file with `https://<user>:<token>@host` for push-work
# SYLPH_LOOP_INTERVAL fixed loop cadence (default 45m)
#
# ── Two hard-won constraints ────────────────────────────────────────────────
#
# 1. THIS REPO IS ITS OWN CLONE. It is deliberately NOT the tree the RE agent
# or a human is working in. Sharing a working tree between two writers means
# files change under whoever is mid-edit, and a `git add -A` by one sweeps up
# the other's work. That happened; do not re-create it.
#
# 2. IDENTITY GOES IN THE ENVIRONMENT, NOT `.git/config`. Writing `[user]` into
# a repo's config captures every commit made in that tree, including a
# human's. GIT_AUTHOR_*/GIT_COMMITTER_* apply to this container's commits and
# nobody else's.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# The repo to mount at /work. Overridable so this script can be run from a
# worktree -- a human editing on `main` must not repoint the agent's checkout.
REPO="${SYLPH_PORT_REPO:-$(cd "$HERE/.." && pwd)}"
IMAGE="${SYLPH_PORT_IMAGE:-sylpheed-port:latest}"
NAME="${SYLPH_PORT_NAME:-sylpheed-port}"
# Half of what the RE container takes. That container builds a C++ emulator and
# drives it; this one converts assets and runs Godot. Two full-size containers
# do not fit on a 12-core / 15 GB box beside a desktop -- memory is the binding
# constraint, and an over-committed build has crashed this machine before.
CPUS="${SYLPH_PORT_CPUS:-3}"
MEM_GB="${SYLPH_PORT_MEM_GB:-4}"
REBORN="${SYLPH_REBORN:-$(cd "$REPO/../Syplheed-Reborn" 2>/dev/null && pwd || true)}"
DISC="${SYLPH_DISC:-$(cd "$REPO/../sylph_extract" 2>/dev/null && pwd || true)}"
docker_args() {
local _out=(
--name "$NAME"
--hostname sylph-port
--cpus "$CPUS"
--memory "${MEM_GB}g"
--memory-swap "${MEM_GB}g" # no swap escape hatch: a swapping build
# thrashes the whole host
--pids-limit 2048
-v "$REPO:/work"
-v "sylpheed-port-target:/sylph-home/port/target-container"
# CARGO_HOME on a volume, not the container overlay: without it the pinned
# decoder source is re-fetched from the network on every fresh container.
-v "sylpheed-port-cargo:/sylph-home/port/.cargo"
-v "${SYLPH_CLAUDE_HOME:-$HOME/.claude}:/sylph-home/port/.claude"
-v "${SYLPH_CLAUDE_JSON:-$HOME/.claude.json}:/sylph-home/port/.claude.host.json:ro"
-e "PROJECT_DIR=/work"
)
# The RE corpus, READ-ONLY. `docs/port/HANDOFF.md` is the contract, and the
# agent also builds sylpheed-cli from here for the reference renderer. Mounted
# ro so a port iteration cannot edit the other agent's repository.
if [ -n "$REBORN" ] && [ -d "$REBORN" ]; then
_out+=(-v "$REBORN:/reborn:ro")
else
echo "==> NOTE: no Syplheed-Reborn checkout found; the agent cannot read" >&2
echo " HANDOFF.md or build the reference renderer. Set SYLPH_REBORN." >&2
fi
if [ -n "$DISC" ] && [ -d "$DISC" ]; then
_out+=(-v "$DISC:/disc:ro" -e "SYLPHEED_DISC=/disc")
else
echo "==> NOTE: no extracted disc found; the exporter has nothing to read." >&2
echo " Set SYLPH_DISC to the directory holding dat/ and hidden/." >&2
fi
# Commits are attributed to the port agent, via the environment so that
# nothing is written into the repository's config. See constraint 2 above.
_out+=(
-e "GIT_AUTHOR_NAME=Sylpheed port agent"
-e "GIT_AUTHOR_EMAIL=port-agent@localhost"
-e "GIT_COMMITTER_NAME=Sylpheed port agent"
-e "GIT_COMMITTER_EMAIL=port-agent@localhost"
)
# Mounted as `.host` and copied to a writable file by the entrypoint, exactly
# like .claude.json. `credential.helper=store` REWRITES its file after a
# successful auth -- it writes a temp file and renames over the target, and
# renaming onto a bind-mount point gives EBUSY, which surfaces as
# `fatal: unable to write credential store: Device or resource busy`.
#
# The push still succeeds, which is the actual danger: a `fatal:` line that is
# routinely wrong teaches the reader to ignore the one that is real. Mounting
# rw would also silence it, but then the container can clobber the host's
# credential file; copying cannot.
local gitcred="${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}"
if [ -f "$gitcred" ]; then
_out+=(-v "$gitcred:/sylph-home/port/.git-credentials.host:ro")
else
echo "==> NOTE: no git credentials at $gitcred — the agent cannot push," >&2
echo " so its work dies with the container." >&2
fi
[ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
printf '%s\n' "${_out[@]}"
}
mapfile -t ARGS < <(docker_args)
case "${1:-}" in
build)
exec docker build -t "$IMAGE" \
--build-arg "AGENT_UID=$(id -u)" --build-arg "AGENT_GID=$(id -g)" "$HERE"
;;
shell)
TTY=(-i); [ -t 0 ] && TTY=(-it)
exec docker run --rm "${TTY[@]}" "${ARGS[@]}" "$IMAGE" bash
;;
loose)
shift
TASK="${1:-}"
if [ -z "$TASK" ]; then
if [ -f "$REPO/docs/loop-task.md" ]; then
TASK="$(cat "$REPO/docs/loop-task.md")"
else
TASK="Work the milestones in docs/MISSION.md."
fi
fi
# A FIXED interval, not self-pacing: the one thing an agent deep in a
# milestone reliably forgets is the bookkeeping after it, and a forgotten
# wake-up silently ends the loop.
INTERVAL="${SYLPH_LOOP_INTERVAL-45m}"
echo "==> loose | cpus=$CPUS mem=${MEM_GB}g pacing=${INTERVAL:-self}"
echo "==> repo: $REPO"
echo "==> reborn: ${REBORN:-<none>} (read-only)"
docker run -d -i -t "${ARGS[@]}" -e SYLPH_AUTONOMOUS=1 -w /work "$IMAGE" \
"/loop ${INTERVAL:+$INTERVAL }$TASK" >/dev/null
echo
echo " running detached as '$NAME'."
echo " ./sylph-port remote link to chat with it from anywhere"
echo " ./sylph-port logs -f follow it"
echo " ./sylph-port attach chat with it locally"
echo " ./sylph-port stop stop it"
;;
logs) shift; exec docker logs "$@" "$NAME" ;;
attach) exec docker attach "$NAME" ;;
stop) exec docker rm -f "$NAME" ;;
remote)
echo "waiting for the session to register" >&2
for _ in $(seq 60); do
# Read the container LOG, not the session transcript. The transcript
# records every command run inside the container -- including this
# lookup -- so grepping it matched our own pattern string back.
url=$(docker logs "$NAME" 2>&1 \
| grep -aoE 'https://claude\.ai/code/session_[A-Za-z0-9]+' \
| tail -1 || true)
[ -n "$url" ] && { echo "$url"; exit 0; }
sleep 2
done
echo "no session link yet — try ./sylph-port logs -f" >&2
exit 1
;;
*)
sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
;;
esac