Files
Sylpheed/docker/port/entrypoint.sh
MechaCat02 20b3c74b2c
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m10s
CI / WASM — Web (push) Failing after 7m31s
CI / Formatting (push) Failing after 1m13s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
agents: they never spoke, the decoder lost the disc, and both shared one state dir
Three defects, all mine, found by checking instead of assuming.

**They never exchanged a word.** SendMessage=0, ListAgents=0 across both new
sessions. PROTOCOL.md specified in detail what a message MAY and MAY NOT do and
never said how to send one or that the other agent was addressable -- they knew
that last time only because the human told them directly, and rebuilding with
fresh volumes wiped it. Policy without mechanism is prose. Now documented with
the two addresses, a worked example, and an instruction to introduce themselves
on the first iteration rather than waiting to have a question.

**The decoder lost the disc and the ISO.** They used to arrive inside the project
mount and silently stopped when /work became a clone. Silently is the word: the
disc-gated tests SELF-SKIP without SYLPHEED_DISC and report green, so a whole
test suite would have passed while measuring nothing. Both are now mounted
explicitly, the ISO at a stable path so run-canary does not depend on host
directory names.

**Both agents shared one Claude state directory.** They share the host's
~/.claude, and once both working directories became /work they resolved to the
same projects/-work/ -- two supposedly independent agents writing to one place,
which undoes the point of separate checkouts. Each now has its own volume, seeded
once from the host with credentials only, so a token refresh writes locally and
neither can corrupt the host's auth.

Also widened the pacing rule. It banned ScheduleWakeup by name; the decoder then
scheduled itself an hourly cron job -- not harmful, but the same instinct that
ended a run yesterday, through a door I had left open. Now: no self-scheduling by
any route.

Mount audit after the changes: shared and intentional are the exchange volume and
the read-only credential seed. Everything else -- repo, Claude state, cargo,
target, canary, disc, ISO -- is per agent or one-sided.
2026-08-29 13:05:41 +02:00

119 lines
6.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Bring up the headless display, then hand over.
#
# Xvfb and openbox are started as children of PID 1 (tini), NOT of the agent's
# shell, so they outlive any single command. The RE container learned this the
# hard way: a display owned by a shell gets reaped when that shell exits, which
# reads as "Xvfb dies on its own every few minutes".
set -euo pipefail
: "${DISPLAY:=:97}"
: "${SCREEN_GEOMETRY:=1280x720x24}"
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
Xvfb "$DISPLAY" -screen 0 "$SCREEN_GEOMETRY" -nolisten tcp &
for _ in $(seq 50); do
xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break
sleep 0.1
done
openbox >/dev/null 2>&1 &
fi
echo "[entrypoint] display $DISPLAY ready ($SCREEN_GEOMETRY)"
# ── Claude state: this agent's own, seeded once from the host ───────────────
# Isolated per agent. Both working directories are /work, and Claude Code keys
# its per-project state off the working directory -- so a SHARED ~/.claude put
# two independent agents in the same projects/-work/ directory, which undoes the
# point of giving them separate checkouts.
#
# Seeded rather than shared because credentials live in .credentials.json and a
# token refresh must be able to write. Copying once means each agent refreshes
# its own token and neither can corrupt the host's.
if [ -d "$HOME/.claude.seed" ] && [ ! -s "$HOME/.claude/.credentials.json" ]; then
mkdir -p "$HOME/.claude"
cp -a "$HOME/.claude.seed/.credentials.json" "$HOME/.claude/" 2>/dev/null || true
for f in settings.json CLAUDE.md; do
[ -e "$HOME/.claude.seed/$f" ] && cp -a "$HOME/.claude.seed/$f" "$HOME/.claude/" 2>/dev/null || true
done
echo "[entrypoint] seeded ~/.claude from the host (credentials only)"
fi
# Seed ~/.claude.json from the host's read-only copy, then stamp onboarding as
# complete. Claude Code re-runs its first-run wizard whenever
# lastOnboardingVersion differs from the installed version, so a container with a
# newer Claude than the host stops on the theme picker -- no error, no log line,
# and an unattended agent sits there forever.
if [ -f "$HOME/.claude.host.json" ] && [ ! -s "$HOME/.claude.json" ]; then
cp "$HOME/.claude.host.json" "$HOME/.claude.json" 2>/dev/null || true
fi
# Same reason as .claude.json above: `credential.helper=store` rewrites this
# file by rename-over-target, which fails with EBUSY on a bind mount. Copy it to
# a writable path; nothing is ever written back to the host's file.
if [ -f "$HOME/.git-credentials.host" ]; then
cp "$HOME/.git-credentials.host" "$HOME/.git-credentials" 2>/dev/null || true
chmod 600 "$HOME/.git-credentials" 2>/dev/null || true
fi
CLAUDE_VER=$(claude --version 2>/dev/null | grep -oE '^[0-9][0-9.]*' || echo 0.0.0)
python3 /usr/local/bin/seed-claude-config.py "$HOME/.claude.json" "$CLAUDE_VER" \
"$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true
chmod 600 "$HOME/.claude.json" 2>/dev/null || true
# ── The repository, cloned into THIS AGENT'S OWN volume ─────────────────────
# Not a bind mount of a human's working tree. That arrangement bit this project
# three times: an agent's `git config --local` captured a human's commits, a
# credential helper leaked a container-only path onto the host, and a `git add
# -A` swept an agent's in-flight files into somebody else's commit. Separate
# checkouts make all three impossible rather than discouraged.
#
# 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.
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"
_tmp=$(mktemp -d)
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
echo "[entrypoint] /work at $(git -C /work rev-parse --short HEAD) on $(git -C /work rev-parse --abbrev-ref HEAD)"
fi
# The shared exchange, for transient files that must not enter git history.
mkdir -p /exchange/files 2>/dev/null || true
# ── Claude Code ──────────────────────────────────────────────────────────────
# Without this the loop prompt is handed to `exec` as a command, and the whole
# markdown file is tried as a filename: exit 126, "File name too long".
if [ "${SYLPH_AUTONOMOUS:-0}" = "1" ]; then
# Drop the image's default CMD first, or `claude` is handed the literal string
# "bash" as its prompt and answers a question nobody asked.
if [ "$#" -eq 1 ] && [ "$1" = "bash" ]; then
set --
fi
# Remote Control registers the session with the account so the agent can be
# reached from claude.ai -- the point of a detached run being that nobody is
# sitting in front of it. The name is passed EXPLICITLY: the flag's value is
# optional, so a bare --remote-control swallows the /loop prompt after it.
if [ "${SYLPH_REMOTE:-1}" != "0" ]; then
set -- --remote-control "${SYLPH_REMOTE_NAME:-sylpheed-port}" "$@"
echo "[entrypoint] Remote Control as '${SYLPH_REMOTE_NAME:-sylpheed-port}'"
fi
# claude-autonomous wraps `claude --dangerously-skip-permissions` in a pty and
# answers the one-time first-run gates. The Bypass Permissions disclaimer has
# no config key that skips it, so unattended it hangs forever.
set -- claude-autonomous "$@"
echo "[entrypoint] starting Claude Code in $(pwd)"
fi
exec "$@"