diff --git a/docker/agent/README.md b/docker/agent/README.md index 8a5cc4a..d63740a 100644 --- a/docker/agent/README.md +++ b/docker/agent/README.md @@ -11,6 +11,8 @@ the emulator, reads its guest memory and photographs its screen. ./sylph-agent shell # poke around ./sylph-agent agent # Claude Code, --dangerously-skip-permissions ./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 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 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 reviews before anything leaves the box. Review with diff --git a/docker/agent/bin/claude-autonomous b/docker/agent/bin/claude-autonomous index c871018..bf64447 100755 --- a/docker/agent/bin/claude-autonomous +++ b/docker/agent/bin/claude-autonomous @@ -28,6 +28,12 @@ set timeout 90 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_trust 0 set answered_bypass 0 diff --git a/docker/agent/entrypoint.sh b/docker/agent/entrypoint.sh index da22398..bc853db 100755 --- a/docker/agent/entrypoint.sh +++ b/docker/agent/entrypoint.sh @@ -115,6 +115,15 @@ if [ "${SYLPH_AUTONOMOUS:-0}" = "1" ]; then 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. diff --git a/docker/agent/sylph-agent b/docker/agent/sylph-agent index 5224bf7..d689b47 100755 --- a/docker/agent/sylph-agent +++ b/docker/agent/sylph-agent @@ -9,6 +9,7 @@ # ./sylph-agent agent [prompt] Claude Code, --dangerously-skip-permissions # ./sylph-agent loose [task] turn it loose: detached, /loop, self-paced # ./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 run one-shot command # ./sylph-agent stop stop it @@ -18,6 +19,8 @@ # SYLPH_CLAUDE_HOME host dir mounted as the agent's ~/.claude # (default: $HOME/.claude — shares auth AND memory with you) # 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 set -euo pipefail @@ -124,6 +127,8 @@ docker_args() { [ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY") [ -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") # ── GPU ── @@ -196,8 +201,9 @@ case "${1:-}" in docker run -d -i -t "${ARGS[@]}" "$IMAGE" "/loop ${INTERVAL:+$INTERVAL }$TASK" >/dev/null echo 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 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 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" ;; - 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) mode=$1; shift