docker: two ways to talk to the loose agent

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>
This commit is contained in:
Sylpheed RE agent
2026-08-18 20:06:05 +02:00
parent f5069b0f64
commit 57a20e6472
4 changed files with 75 additions and 4 deletions

View File

@@ -11,6 +11,8 @@ the emulator, reads its guest memory and photographs its screen.
./sylph-agent shell # poke around ./sylph-agent shell # poke around
./sylph-agent agent # Claude Code, --dangerously-skip-permissions ./sylph-agent agent # Claude Code, --dangerously-skip-permissions
./sylph-agent loose # turn it loose: detached, /loop, self-paced ./sylph-agent loose # turn it loose: detached, /loop, self-paced
./sylph-agent remote # link to chat with it from anywhere
./sylph-agent attach # chat with it locally
./sylph-agent logs -f # watch it ./sylph-agent logs -f # watch it
./sylph-agent stop # stop it ./sylph-agent stop # stop it
``` ```
@@ -26,8 +28,28 @@ fixed cadence instead of letting it self-pace.
It runs `-d` **without** `--rm`, so the transcript survives the container It runs `-d` **without** `--rm`, so the transcript survives the container
exiting — for an unattended run that is the only record of what happened. exiting — for an unattended run that is the only record of what happened.
`./sylph-agent attach` joins the live session (Ctrl-P Ctrl-Q to leave it
running). ### Talking to it
Two channels, both live while it works:
* **`./sylph-agent remote`** prints a `https://claude.ai/code/session_…` link.
`loose` starts Claude Code with `--remote-control`, so the session registers
with your account and you can chat with it from claude.ai or your phone —
which is the point of a detached run. Disable with `SYLPH_REMOTE=0`, rename
with `SYLPH_REMOTE_NAME`.
Registration takes a minute or two after launch, so `remote` waits for it
rather than reporting "not found" to what is really "not yet".
* **`./sylph-agent attach`** joins the container's own terminal. Type to talk to
it; **Ctrl-P Ctrl-Q** detaches and leaves it running. Do not press Ctrl-C —
that goes to the agent.
The pty is forced to 200×50 (`stty_init` in `bin/claude-autonomous`). A detached
`docker run -t` is 80×24, and Claude Code hard-wraps to the terminal width,
which truncated the Remote Control URL to `…/session_01…` in the one place you
need to read it — and made `docker logs` almost unreadable besides.
**It cannot push.** No git credentials are mounted, deliberately: a human **It cannot push.** No git credentials are mounted, deliberately: a human
reviews before anything leaves the box. Review with reviews before anything leaves the box. Review with

View File

@@ -28,6 +28,12 @@
set timeout 90 set timeout 90
log_user 1 log_user 1
# Give the pty a wide, tall geometry. A detached `docker run -t` defaults to
# 80x24, and Claude Code hard-wraps to the terminal width — which truncates the
# Remote Control URL to "https://claude.ai/code/session_01…" in the one place
# you need to read it, and makes `docker logs` nearly unusable generally.
set stty_init "rows 50 cols 200"
set answered_theme 0 set answered_theme 0
set answered_trust 0 set answered_trust 0
set answered_bypass 0 set answered_bypass 0

View File

@@ -115,6 +115,15 @@ if [ "${SYLPH_AUTONOMOUS:-0}" = "1" ]; then
fi fi
# The flag the user asked for. It is refused under root, which is why this # The flag the user asked for. It is refused under root, which is why this
# image runs as `agent`. # 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 # claude-autonomous wraps `claude --dangerously-skip-permissions` in a pty and
# answers the one-time first-run gates. The Bypass Permissions disclaimer in # 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. # particular has no config key that skips it, so unattended it hangs forever.

View File

@@ -9,6 +9,7 @@
# ./sylph-agent agent [prompt] Claude Code, --dangerously-skip-permissions # ./sylph-agent agent [prompt] Claude Code, --dangerously-skip-permissions
# ./sylph-agent loose [task] turn it loose: detached, /loop, self-paced # ./sylph-agent loose [task] turn it loose: detached, /loop, self-paced
# ./sylph-agent logs [-f] what the loose agent is doing # ./sylph-agent logs [-f] what the loose agent is doing
# ./sylph-agent remote print the Remote Control link (chat from anywhere)
# ./sylph-agent attach attach to the loose agent's session # ./sylph-agent attach attach to the loose agent's session
# ./sylph-agent run <cmd...> one-shot command # ./sylph-agent run <cmd...> one-shot command
# ./sylph-agent stop stop it # ./sylph-agent stop stop it
@@ -18,6 +19,8 @@
# SYLPH_CLAUDE_HOME host dir mounted as the agent's ~/.claude # SYLPH_CLAUDE_HOME host dir mounted as the agent's ~/.claude
# (default: $HOME/.claude — shares auth AND memory with you) # (default: $HOME/.claude — shares auth AND memory with you)
# SYLPH_VULKAN=sw force software Vulkan (lavapipe) even if /dev/dri exists # SYLPH_VULKAN=sw force software Vulkan (lavapipe) even if /dev/dri exists
# SYLPH_REMOTE=0 do NOT enable Remote Control (default: enabled for `loose`)
# SYLPH_REMOTE_NAME Remote Control session name (default: sylpheed-agent)
# SYLPH_CPUS / SYLPH_MEM_GB override the computed half # SYLPH_CPUS / SYLPH_MEM_GB override the computed half
set -euo pipefail set -euo pipefail
@@ -124,6 +127,8 @@ docker_args() {
[ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY") [ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
[ -n "${SYLPH_VULKAN:-}" ] && _out+=(-e "SYLPH_VULKAN=$SYLPH_VULKAN") [ -n "${SYLPH_VULKAN:-}" ] && _out+=(-e "SYLPH_VULKAN=$SYLPH_VULKAN")
[ -n "${SYLPH_REMOTE:-}" ] && _out+=(-e "SYLPH_REMOTE=$SYLPH_REMOTE")
[ -n "${SYLPH_REMOTE_NAME:-}" ] && _out+=(-e "SYLPH_REMOTE_NAME=$SYLPH_REMOTE_NAME")
[ -n "${SYLPH_ISO:-}" ] && _out+=(-e "SYLPH_ISO=$SYLPH_ISO") [ -n "${SYLPH_ISO:-}" ] && _out+=(-e "SYLPH_ISO=$SYLPH_ISO")
# ── GPU ── # ── GPU ──
@@ -196,8 +201,9 @@ case "${1:-}" in
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
echo echo
echo " running detached as '$NAME'." echo " running detached as '$NAME'."
echo " ./sylph-agent remote link to chat with it from anywhere"
echo " ./sylph-agent logs -f follow it" echo " ./sylph-agent logs -f follow it"
echo " ./sylph-agent attach attach to the session (Ctrl-P Ctrl-Q to detach)" echo " ./sylph-agent attach chat with it locally (Ctrl-P Ctrl-Q to leave it running)"
echo " ./sylph-agent stop stop it" echo " ./sylph-agent stop stop it"
echo echo
echo " It commits to auto/* branches and cannot push — no git credentials" echo " It commits to auto/* branches and cannot push — no git credentials"
@@ -205,7 +211,35 @@ case "${1:-}" in
;; ;;
logs) shift; exec docker logs "$@" "$NAME" ;; logs) shift; exec docker logs "$@" "$NAME" ;;
attach) exec docker attach "$NAME" ;;
remote)
# Fish the Remote Control link out of the session's own output. Claude Code
# prints it once, when the session registers with your account — which takes
# a minute or two after launch, so this waits rather than answering "not
# found" to a question that is really "not yet".
printf 'waiting for the session to register' >&2
for i in $(seq 1 90); do
url=$(docker logs "$NAME" 2>&1 \
| sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\r//g' \
| grep -oE 'https://claude\.ai/code/[A-Za-z0-9_-]+' | tail -1)
if [ -n "${url:-}" ]; then
printf '\n' >&2
echo "$url"
exit 0
fi
docker ps -q -f "name=$NAME" | grep -q . || {
printf '\n' >&2
echo "container is not running — start it with: ./sylph-agent loose" >&2
exit 1
}
printf '.' >&2
sleep 4
done
printf '\n' >&2
echo "no Remote Control URL after 6 minutes." >&2
echo " Launched with SYLPH_REMOTE=0? Or check: ./sylph-agent logs | tail" >&2
exit 1
;;
shell|agent|run) shell|agent|run)
mode=$1; shift mode=$1; shift