containers: fix volume ownership and make the clone guard survive interruption

Two bugs, both mine, both found by starting the thing.

**Volume mount points must exist AND be owned by the agent before USER agent.**
Docker seeds a named volume from whatever the image has at that path, ownership
included, and creates a ROOT-OWNED directory when the path is absent. Either way
the agent cannot write, and the failure surfaced far from its cause: "clone
FAILED", with no permission error anywhere in sight. The port's own Dockerfile
already carried a comment explaining this trap, which I then walked into for
/work and /exchange.

**The clone guard checked for a .git directory, not a usable HEAD.** A clone
interrupted partway -- the container was removed while one ran -- leaves a .git
with no commits, and a presence check then skips the retry forever and hands the
agent an empty repository that looks like a checkout. It now verifies HEAD, and
clones via a temp directory so a partial result never lands in /work at all.

Also: the port launcher's path defaults still assumed the old repo root, so it
mounted no disc; and the stale /reborn notice is gone now that there is one
repository.

Verified running: both agents cloned c58196b, `share` on PATH from /work/tools,
/exchange agent-owned, canary at /canary for the decoder, disc at /disc for the
port.
This commit is contained in:
MechaCat02
2026-08-29 12:52:13 +02:00
parent c58196b795
commit 824b150be4
6 changed files with 42 additions and 20 deletions

View File

@@ -92,8 +92,8 @@ RUN if getent passwd "${AGENT_UID}" >/dev/null; then \
fi; \ fi; \
groupadd -g "${AGENT_GID}" agent \ groupadd -g "${AGENT_GID}" agent \
&& useradd -m -u "${AGENT_UID}" -g "${AGENT_GID}" -s /bin/bash -d /sylph-home/re agent \ && useradd -m -u "${AGENT_UID}" -g "${AGENT_GID}" -s /bin/bash -d /sylph-home/re agent \
&& mkdir -p /sylph-home/re /work \ && mkdir -p /sylph-home/re /work /exchange \
&& chown -R "${AGENT_UID}:${AGENT_GID}" /sylph-home \ && chown -R "${AGENT_UID}:${AGENT_GID}" /sylph-home /work /exchange \
&& echo 'agent ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/agent && echo 'agent ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/agent
COPY bin/ /usr/local/bin/ COPY bin/ /usr/local/bin/

View File

@@ -101,12 +101,23 @@ mkdir -p "$HOME/shots" "$HOME/logs"
# #
# Cloned ONCE. Never auto-pulled: pulling under a running agent moves files out # Cloned ONCE. Never auto-pulled: pulling under a running agent moves files out
# from under whatever it is mid-edit, which is the same class of bug again. # from under whatever it is mid-edit, which is the same class of bug again.
if [ ! -d /work/.git ]; then if ! git -C /work rev-parse --verify HEAD >/dev/null 2>&1; then
# Checks for a usable HEAD, not merely a .git directory. A clone interrupted
# partway -- the container stopped while it ran, which has happened -- leaves
# a .git with no commits, and a presence check would then skip the retry
# forever and hand the agent an empty repository.
echo "[entrypoint] cloning ${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git} into /work" echo "[entrypoint] cloning ${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git} into /work"
git clone --quiet "${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git}" /work || { _tmp=$(mktemp -d)
echo "[entrypoint] clone FAILED -- the agent has no repository" >&2; } if git clone --quiet "${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git}" "$_tmp/r"; then
find /work -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
mv "$_tmp/r"/.[!.]* "$_tmp/r"/* /work/ 2>/dev/null || true
echo "[entrypoint] /work at $(git -C /work rev-parse --short HEAD) on $(git -C /work rev-parse --abbrev-ref HEAD)"
else
echo "[entrypoint] clone FAILED -- the agent has no repository" >&2
fi
rm -rf "$_tmp"
else else
echo "[entrypoint] /work is at $(git -C /work rev-parse --short HEAD 2>/dev/null) on $(git -C /work rev-parse --abbrev-ref HEAD 2>/dev/null)" echo "[entrypoint] /work at $(git -C /work rev-parse --short HEAD) on $(git -C /work rev-parse --abbrev-ref HEAD)"
fi fi
# The shared exchange, for transient files that must not enter git history. # The shared exchange, for transient files that must not enter git history.

View File

@@ -234,9 +234,9 @@ case "${1:-}" in
# wakeup cannot end the run. Set SYLPH_LOOP_INTERVAL= (empty) to self-pace. # wakeup cannot end the run. Set SYLPH_LOOP_INTERVAL= (empty) to self-pace.
INTERVAL="${SYLPH_LOOP_INTERVAL-45m}" INTERVAL="${SYLPH_LOOP_INTERVAL-45m}"
declare -a ARGS; docker_args ARGS declare -a ARGS; docker_args ARGS
ARGS+=(-e SYLPH_AUTONOMOUS=1 -w "$PROJECT") ARGS+=(-e SYLPH_AUTONOMOUS=1 -w /work)
echo "==> loose | cpus=$CPUS mem=${MEM_GB}g shm=${SHM_GB}g" echo "==> loose | cpus=$CPUS mem=${MEM_GB}g shm=${SHM_GB}g"
echo "==> project: $PROJECT (mounted at its own path, so memory carries over)" echo "==> repo: own clone in volume sylpheed-decoder-repo -> /work"
echo "==> pacing: ${INTERVAL:-self-paced}" echo "==> pacing: ${INTERVAL:-self-paced}"
docker rm -f "$NAME" >/dev/null 2>&1 || true docker rm -f "$NAME" >/dev/null 2>&1 || true
docker run -d -i -t "${ARGS[@]}" "$IMAGE" "/loop ${INTERVAL:+$INTERVAL }$TASK" >/dev/null docker run -d -i -t "${ARGS[@]}" "$IMAGE" "/loop ${INTERVAL:+$INTERVAL }$TASK" >/dev/null

View File

@@ -69,8 +69,8 @@ RUN if getent passwd "${AGENT_UID}" >/dev/null; then \
fi; \ fi; \
groupadd -g "${AGENT_GID}" agent \ groupadd -g "${AGENT_GID}" agent \
&& useradd -m -u "${AGENT_UID}" -g "${AGENT_GID}" -s /bin/bash -d /sylph-home/port agent \ && useradd -m -u "${AGENT_UID}" -g "${AGENT_GID}" -s /bin/bash -d /sylph-home/port agent \
&& mkdir -p /sylph-home/port /work /reborn \ && mkdir -p /sylph-home/port /work /exchange /reborn \
&& chown -R "${AGENT_UID}:${AGENT_GID}" /sylph-home \ && chown -R "${AGENT_UID}:${AGENT_GID}" /sylph-home /work /exchange \
&& echo 'agent ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/agent && echo 'agent ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/agent
COPY bin/ /usr/local/bin/ COPY bin/ /usr/local/bin/

View File

@@ -20,9 +20,6 @@ if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
fi fi
echo "[entrypoint] display $DISPLAY ready ($SCREEN_GEOMETRY)" echo "[entrypoint] display $DISPLAY ready ($SCREEN_GEOMETRY)"
if [ -d /reborn ]; then
echo "[entrypoint] /reborn mounted read-only — HANDOFF.md is the contract"
fi
# Seed ~/.claude.json from the host's read-only copy, then stamp onboarding as # Seed ~/.claude.json from the host's read-only copy, then stamp onboarding as
# complete. Claude Code re-runs its first-run wizard whenever # complete. Claude Code re-runs its first-run wizard whenever
@@ -54,12 +51,23 @@ chmod 600 "$HOME/.claude.json" 2>/dev/null || true
# #
# Cloned ONCE. Never auto-pulled: pulling under a running agent moves files out # Cloned ONCE. Never auto-pulled: pulling under a running agent moves files out
# from under whatever it is mid-edit, which is the same class of bug again. # from under whatever it is mid-edit, which is the same class of bug again.
if [ ! -d /work/.git ]; then if ! git -C /work rev-parse --verify HEAD >/dev/null 2>&1; then
# Checks for a usable HEAD, not merely a .git directory. A clone interrupted
# partway -- the container stopped while it ran, which has happened -- leaves
# a .git with no commits, and a presence check would then skip the retry
# forever and hand the agent an empty repository.
echo "[entrypoint] cloning ${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git} into /work" echo "[entrypoint] cloning ${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git} into /work"
git clone --quiet "${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git}" /work || { _tmp=$(mktemp -d)
echo "[entrypoint] clone FAILED -- the agent has no repository" >&2; } if git clone --quiet "${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git}" "$_tmp/r"; then
find /work -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
mv "$_tmp/r"/.[!.]* "$_tmp/r"/* /work/ 2>/dev/null || true
echo "[entrypoint] /work at $(git -C /work rev-parse --short HEAD) on $(git -C /work rev-parse --abbrev-ref HEAD)"
else
echo "[entrypoint] clone FAILED -- the agent has no repository" >&2
fi
rm -rf "$_tmp"
else else
echo "[entrypoint] /work is at $(git -C /work rev-parse --short HEAD 2>/dev/null) on $(git -C /work rev-parse --abbrev-ref HEAD 2>/dev/null)" echo "[entrypoint] /work at $(git -C /work rev-parse --short HEAD) on $(git -C /work rev-parse --abbrev-ref HEAD)"
fi fi
# The shared exchange, for transient files that must not enter git history. # The shared exchange, for transient files that must not enter git history.

View File

@@ -32,7 +32,10 @@ set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# The repo to mount at /work. Overridable so this script can be run from a # 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. # worktree -- a human editing on `main` must not repoint the agent's checkout.
REPO="${SYLPH_PORT_REPO:-$(cd "$HERE/.." && pwd)}" # The checkout is now a volume the container clones into, so this is only used
# to locate things that live BESIDE the repository -- the disc, chiefly. Three
# levels up from docker/port/ is the workspace root.
WORKSPACE="$(cd "$HERE/../../.." && pwd)"
IMAGE="${SYLPH_PORT_IMAGE:-sylpheed-port:latest}" IMAGE="${SYLPH_PORT_IMAGE:-sylpheed-port:latest}"
NAME="${SYLPH_PORT_NAME:-sylpheed-port}" NAME="${SYLPH_PORT_NAME:-sylpheed-port}"
@@ -43,7 +46,7 @@ NAME="${SYLPH_PORT_NAME:-sylpheed-port}"
CPUS="${SYLPH_PORT_CPUS:-3}" CPUS="${SYLPH_PORT_CPUS:-3}"
MEM_GB="${SYLPH_PORT_MEM_GB:-4}" MEM_GB="${SYLPH_PORT_MEM_GB:-4}"
DISC="${SYLPH_DISC:-$(cd "$REPO/../sylph_extract" 2>/dev/null && pwd || true)}" DISC="${SYLPH_DISC:-$(cd "$WORKSPACE/sylph_extract" 2>/dev/null && pwd || true)}"
docker_args() { docker_args() {
local _out=( local _out=(
@@ -135,7 +138,7 @@ case "${1:-}" in
# wake-up silently ends the loop. # wake-up silently ends the loop.
INTERVAL="${SYLPH_LOOP_INTERVAL-45m}" INTERVAL="${SYLPH_LOOP_INTERVAL-45m}"
echo "==> loose | cpus=$CPUS mem=${MEM_GB}g pacing=${INTERVAL:-self}" echo "==> loose | cpus=$CPUS mem=${MEM_GB}g pacing=${INTERVAL:-self}"
echo "==> repo: $REPO" echo "==> repo: own clone in volume sylpheed-port-repo -> /work"
docker run -d -i -t "${ARGS[@]}" -e SYLPH_AUTONOMOUS=1 -w /work "$IMAGE" \ docker run -d -i -t "${ARGS[@]}" -e SYLPH_AUTONOMOUS=1 -w /work "$IMAGE" \
"/loop ${INTERVAL:+$INTERVAL }$TASK" >/dev/null "/loop ${INTERVAL:+$INTERVAL }$TASK" >/dev/null
echo echo