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:
@@ -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/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user