Remote Control, so a detached run is not a one-way trip: ./sylph-agent remote -> https://claude.ai/code/session_... ./sylph-agent attach -> the container's own terminal `loose` now starts Claude Code with `--remote-control <name>`, registering the session with your account so you can chat with it from claude.ai or a phone. The name is passed EXPLICITLY because the flag's value is optional — a bare `--remote-control` swallows the /loop prompt that follows as the session name. Off with SYLPH_REMOTE=0, renamed with SYLPH_REMOTE_NAME. The pty is forced to 200x50. A detached `docker run -t` gives 80x24, and Claude Code hard-wraps to the terminal width — which truncated the Remote Control URL to ".../session_01..." in the one place you actually need to read it, and made `docker logs` nearly unusable besides. `remote` waits up to six minutes and reports progress: registration lands a minute or two after launch, so answering "not found" immediately would be answering a different question than the one being asked. Verified: a loose run registers and `./sylph-agent remote` prints the full URL. `attach` is NOT verified end to end from here — `docker attach` refuses a piped stdin ("cannot attach stdin to a TTY-enabled container"), which is a property of this harness rather than of the container, so it needs a real terminal to try. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
135 lines
6.9 KiB
Bash
Executable File
135 lines
6.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Bring up the headless display, then hand over to the command.
|
|
#
|
|
# Xvfb and openbox are started HERE, as children of PID 1 (tini), rather than by
|
|
# the toolkit scripts. That is the fix for the long-standing "Xvfb and the
|
|
# emulator die on their own every few minutes" note: nothing owned those
|
|
# processes, so nothing kept them alive, and a run could sit for 300 s in front
|
|
# of a visible MAIN MENU reporting "no main menu" because the display had gone.
|
|
# A container-lifetime display makes that failure mode impossible.
|
|
set -euo pipefail
|
|
|
|
log() { printf '[entrypoint] %s\n' "$*" >&2; }
|
|
|
|
DISPLAY="${DISPLAY:-:98}"
|
|
GEOM="${SCREEN_GEOMETRY:-1280x720x24}"
|
|
export DISPLAY
|
|
|
|
# ── Display ──────────────────────────────────────────────────────────────────
|
|
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
|
|
rm -f "/tmp/.X${DISPLAY#:}-lock" "/tmp/.X11-unix/X${DISPLAY#:}" 2>/dev/null || true
|
|
# GLX and RANDR are both required: Canary's window is GTK+OpenGL even when the
|
|
# graphics backend is Vulkan, and xwininfo-based screen oracles need RANDR.
|
|
Xvfb "$DISPLAY" -screen 0 "$GEOM" -ac -nolisten tcp \
|
|
+extension GLX +extension RANDR >/tmp/xvfb.log 2>&1 &
|
|
for _ in $(seq 1 50); do
|
|
xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break
|
|
sleep 0.2
|
|
done
|
|
fi
|
|
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
|
|
log "FATAL: no display on $DISPLAY — see /tmp/xvfb.log"
|
|
exit 1
|
|
fi
|
|
|
|
# A window manager is not cosmetic here: without one the emulator window is
|
|
# never mapped at a known position, and every pixel oracle reads the wrong rows.
|
|
if ! pgrep -x openbox >/dev/null 2>&1; then
|
|
openbox >/tmp/openbox.log 2>&1 &
|
|
sleep 0.5
|
|
fi
|
|
log "display $DISPLAY ready ($GEOM)"
|
|
|
|
# ── Vulkan ───────────────────────────────────────────────────────────────────
|
|
# Prefer the real GPU when /dev/dri was passed through; fall back to lavapipe,
|
|
# which is slow but correct and needs no host device.
|
|
LVP=$(ls /usr/share/vulkan/icd.d/lvp_icd*.json 2>/dev/null | head -1)
|
|
if [ "${SYLPH_VULKAN:-auto}" = "sw" ] || [ ! -e /dev/dri/renderD128 ]; then
|
|
[ -n "$LVP" ] && export VK_ICD_FILENAMES="$LVP"
|
|
else
|
|
unset LIBGL_ALWAYS_SOFTWARE
|
|
fi
|
|
# Report what Vulkan ACTUALLY enumerated, not what we asked for. Announcing
|
|
# "host GPU via /dev/dri" because the device node exists is how an agent ends up
|
|
# believing it has hardware while running llvmpipe — an NVIDIA card needs the
|
|
# NVIDIA Container Toolkit, and /dev/dri alone does nothing for it.
|
|
if command -v vulkaninfo >/dev/null 2>&1; then
|
|
vkdev=$(vulkaninfo --summary 2>/dev/null | awk -F= '/deviceName/{gsub(/^ +| +$/,"",$2); print $2; exit}')
|
|
case "${vkdev:-}" in
|
|
"") log "Vulkan: NO DEVICE — vulkaninfo enumerated nothing" ;;
|
|
llvmpipe*|lavapipe*) log "Vulkan: $vkdev (SOFTWARE — correct but slow)" ;;
|
|
*) log "Vulkan: $vkdev (hardware)" ;;
|
|
esac
|
|
fi
|
|
|
|
# ── Build parallelism ────────────────────────────────────────────────────────
|
|
# Bounded by MEMORY, not just cores. A full-parallel build of this tree has
|
|
# OOM-killed the host outright, and inside a half-the-box container the ceiling
|
|
# is lower still. ~1.5 GiB per C++ TU is the rule of thumb that has held.
|
|
# nproc reports the HOST's core count: --cpus is a CFS quota, not a mask. Using
|
|
# it would oversubscribe a half-the-box container by exactly 2x, so read the
|
|
# quota the cgroup actually grants.
|
|
cpus=$(nproc)
|
|
if [ -r /sys/fs/cgroup/cpu.max ]; then
|
|
read -r _q _p < /sys/fs/cgroup/cpu.max || true
|
|
if [ "${_q:-max}" != max ] && [ "${_p:-0}" -gt 0 ]; then
|
|
cpus=$(( (_q + _p - 1) / _p ))
|
|
[ "$cpus" -lt 1 ] && cpus=1
|
|
fi
|
|
fi
|
|
mem_gib=$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)
|
|
# MemAvailable is the HOST's too under cgroup v2; prefer the container's cap.
|
|
if [ -r /sys/fs/cgroup/memory.max ]; then
|
|
_m=$(cat /sys/fs/cgroup/memory.max)
|
|
[ "$_m" != max ] && mem_gib=$(( _m / 1073741824 ))
|
|
fi
|
|
[ "${mem_gib:-0}" -lt 1 ] && mem_gib=1
|
|
by_mem=$(( mem_gib * 2 / 3 ))
|
|
[ "$by_mem" -lt 1 ] && by_mem=1
|
|
jobs=$(( cpus < by_mem ? cpus : by_mem ))
|
|
export SYLPH_JOBS="$jobs" CARGO_BUILD_JOBS="$jobs" CMAKE_BUILD_PARALLEL_LEVEL="$jobs"
|
|
log "build parallelism: $jobs (cpus=$cpus, mem=${mem_gib}GiB avail)"
|
|
|
|
mkdir -p "$HOME/shots" "$HOME/logs"
|
|
|
|
# ── Claude Code config ───────────────────────────────────────────────────────
|
|
# 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, with no error and 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
|
|
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
|
|
|
|
# ── Claude Code ──────────────────────────────────────────────────────────────
|
|
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
|
|
# The flag the user asked for. It is refused under root, which is why this
|
|
# image runs as `agent`.
|
|
# Remote Control registers the session with your account so you can chat with
|
|
# the agent from claude.ai or your phone — the point of a detached run being
|
|
# that you are not sitting in front of it. The name is passed EXPLICITLY: the
|
|
# flag's value is optional, so a bare `--remote-control` would swallow the
|
|
# /loop prompt that follows as the session name.
|
|
if [ "${SYLPH_REMOTE:-1}" != "0" ]; then
|
|
set -- --remote-control "${SYLPH_REMOTE_NAME:-sylpheed-agent}" "$@"
|
|
log "Remote Control enabled as '${SYLPH_REMOTE_NAME:-sylpheed-agent}'"
|
|
fi
|
|
# claude-autonomous wraps `claude --dangerously-skip-permissions` in a pty and
|
|
# answers the one-time first-run gates. The Bypass Permissions disclaimer in
|
|
# particular has no config key that skips it, so unattended it hangs forever.
|
|
set -- claude-autonomous "$@"
|
|
log "starting Claude Code with --dangerously-skip-permissions in $(pwd)"
|
|
fi
|
|
|
|
exec "$@"
|