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>
This commit is contained in:
MechaCat02
2026-08-28 21:07:13 +02:00
parent a1d0e8e2c3
commit 0fccea389d
3 changed files with 36 additions and 4 deletions

View File

@@ -32,6 +32,14 @@ fi
if [ -f "$HOME/.claude.host.json" ] && [ ! -s "$HOME/.claude.json" ]; then if [ -f "$HOME/.claude.host.json" ] && [ ! -s "$HOME/.claude.json" ]; then
cp "$HOME/.claude.host.json" "$HOME/.claude.json" 2>/dev/null || true cp "$HOME/.claude.host.json" "$HOME/.claude.json" 2>/dev/null || true
fi 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) 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" \ python3 /usr/local/bin/seed-claude-config.py "$HOME/.claude.json" "$CLAUDE_VER" \
"$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true "$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true

View File

@@ -11,6 +11,7 @@
# #
# Env: # Env:
# SYLPH_PORT_CPUS / SYLPH_PORT_MEM_GB override the cap (default 3 / 4) # 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_REBORN path to the Syplheed-Reborn checkout (read-only mount)
# SYLPH_DISC extracted disc root # SYLPH_DISC extracted disc root
# SYLPH_GIT_CREDENTIALS file with `https://<user>:<token>@host` for push-work # SYLPH_GIT_CREDENTIALS file with `https://<user>:<token>@host` for push-work
@@ -30,7 +31,9 @@
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/.." && 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}" IMAGE="${SYLPH_PORT_IMAGE:-sylpheed-port:latest}"
NAME="${SYLPH_PORT_NAME:-sylpheed-port}" NAME="${SYLPH_PORT_NAME:-sylpheed-port}"
@@ -55,6 +58,9 @@ docker_args() {
--pids-limit 2048 --pids-limit 2048
-v "$REPO:/work" -v "$REPO:/work"
-v "sylpheed-port-target:/sylph-home/port/target-container" -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_HOME:-$HOME/.claude}:/sylph-home/port/.claude"
-v "${SYLPH_CLAUDE_JSON:-$HOME/.claude.json}:/sylph-home/port/.claude.host.json:ro" -v "${SYLPH_CLAUDE_JSON:-$HOME/.claude.json}:/sylph-home/port/.claude.host.json:ro"
-e "PROJECT_DIR=/work" -e "PROJECT_DIR=/work"
@@ -86,9 +92,19 @@ docker_args() {
-e "GIT_COMMITTER_EMAIL=port-agent@localhost" -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}" local gitcred="${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}"
if [ -f "$gitcred" ]; then if [ -f "$gitcred" ]; then
_out+=(-v "$gitcred:/sylph-home/port/.git-credentials:ro") _out+=(-v "$gitcred:/sylph-home/port/.git-credentials.host:ro")
else else
echo "==> NOTE: no git credentials at $gitcred — the agent cannot push," >&2 echo "==> NOTE: no git credentials at $gitcred — the agent cannot push," >&2
echo " so its work dies with the container." >&2 echo " so its work dies with the container." >&2

View File

@@ -16,8 +16,16 @@ believed later. If you need an answer the disc has not given you, write it in
1. `docs/MISSION.md` — milestones, gates, scope. 1. `docs/MISSION.md` — milestones, gates, scope.
2. `/reborn/docs/port/HANDOFF.md`**the contract.** What is decoded, what was 2. `/reborn/docs/port/HANDOFF.md`**the contract.** What is decoded, what was
measured off the running game, and what is known undecodable. `git -C /reborn measured off the running game, and what is known undecodable.
pull` first; the RE agent publishes continuously.
**It is a live read-only mount of the RE agent's working tree**, so it updates
itself and there is nothing to pull — `git -C /reborn pull` cannot work (the
mount is read-only) and should not: it would move another agent's checkout.
`git -C /reborn log -1` shows where they are.
Because it is live, **it can move under you mid-iteration.** Anything you
copied out of it earlier — `docs/BLOCKED.md` especially — may already be
stale. Re-check it against HANDOFF before trusting it.
3. `docs/FORMAT.md` — the open format. It is versioned and it is yours to 3. `docs/FORMAT.md` — the open format. It is versioned and it is yours to
revise, but a change is a deliberate act with a version bump. revise, but a change is a deliberate act with a version bump.
4. `docs/BLOCKED.md` — what you are waiting on, so you do not re-discover it. 4. `docs/BLOCKED.md` — what you are waiting on, so you do not re-discover it.