docker: stop the wrapper typing into live sessions, and support per-agent logins
Both agents stopped, and the decoder diagnosed it itself:
"I received '2' and '1' but I don't have a pending question those would
answer -- I was in the middle of setting up the /loop cron job."
claude-autonomous matched the BARE SUBSTRINGS 'Choose', 'trust' and 'accept' to
answer Claude Code's one-time first-run gates. The /loop prompt is echoed into
the terminal, and that day's briefs contain 'accepted as-is' and 'least
trustworthy' -- so expect matched the agent's OWN INSTRUCTIONS and typed 2\r and
1\r into a running session, which then sat waiting for a human to explain them.
The old comment argued a multi-word pattern 'never matches' because the gate
text wraps. True of a literal string, false of a whitespace-tolerant regex, which
is what these now are: \s+ spans the wrap, and the terminal is 200 columns wide.
Measured, old against new, against the real brief text and a real gate:
{accept} brief 0 gate 1 (case-sensitive; briefs say 'accepted')
{Yes,\s*I\s+accept} brief 0 gate 1
{trust} brief 1 <- the trigger
{Do\s+you\s+trust\s+the\s+files} brief 0
Two defences, because one is not enough for something that can type: patterns
prose cannot match, and gates skipped ENTIRELY on resume (SYLPH_SKIP_GATES) --
a resumed session cannot show a first-run gate, so there is nothing to answer
and everything to lose. Timeout cut 90s -> 25s for the same reason.
Also: SYLPH_OWN_LOGIN. Remote Control stopped registering under the long-lived
token, and the likely reason is scope -- `claude auth login` requests
user:sessions:claude_code and the token's auth status reports no email, org or
subscription. A per-agent `claude auth login` restores Remote Control AND avoids
the rotation collision, because each agent holds its own grant rather than a copy
of one. The flag stops the entrypoint seeding the host's credentials over it.
This commit is contained in:
@@ -78,32 +78,62 @@ trap { forward TERM } SIGTERM
|
||||
trap { forward INT } SIGINT
|
||||
trap { forward HUP } SIGHUP
|
||||
|
||||
expect {
|
||||
-re {Choose} {
|
||||
if {!$answered_theme} { set answered_theme 1; send "\r" }
|
||||
exp_continue
|
||||
}
|
||||
-re {trust} {
|
||||
if {!$answered_trust} {
|
||||
set answered_trust 1
|
||||
send_user "\n\[claude-autonomous] accepting the workspace trust prompt\n"
|
||||
send "1\r"
|
||||
# 🔴 THIS BLOCK TYPED INTO A LIVE SESSION, and the single-word patterns were why.
|
||||
#
|
||||
# 2026-09-04: both agents stopped, and the decoder said so itself --
|
||||
#
|
||||
# "I received '2' and '1' but I don't have a pending question those would
|
||||
# answer -- I was in the middle of setting up the /loop cron job."
|
||||
#
|
||||
# The patterns were the bare substrings `Choose`, `trust` and `accept`. The
|
||||
# /loop PROMPT is echoed into the terminal, and that day's brief contained
|
||||
# "H3, the plate delay, is ACCEPTED" and "Do not choose what jump means". So
|
||||
# expect matched the agent's own instructions and sent `2\r` and `1\r` into a
|
||||
# running session, which then sat waiting for a human to explain them.
|
||||
#
|
||||
# The original comment argued that a multi-word pattern "never matches" because
|
||||
# the gate text wraps. That is true of a LITERAL multi-word string and false of a
|
||||
# whitespace-tolerant regex, which is what these now are: `\s+` spans the wrap.
|
||||
# The terminal is also 200 columns wide (set above), so these lines rarely wrap
|
||||
# at all.
|
||||
#
|
||||
# Two defences, because one is not enough for something that can type:
|
||||
# 1. patterns specific enough that ordinary prose cannot match them
|
||||
# 2. gates are skipped ENTIRELY when resuming -- a resumed session cannot show
|
||||
# a first-run gate, so there is nothing to answer and everything to lose
|
||||
if {[info exists env(SYLPH_SKIP_GATES)] && $env(SYLPH_SKIP_GATES) ne "0"} {
|
||||
send_user "\[claude-autonomous] resuming: first-run gates cannot appear, not watching for them\n"
|
||||
} else {
|
||||
# Shorter than the old 90 s. The gates appear immediately or not at all, and
|
||||
# every extra second is a second in which this can type into a live session.
|
||||
set timeout 25
|
||||
expect {
|
||||
-re {Choose\s+the\s+text\s+style} {
|
||||
if {!$answered_theme} { set answered_theme 1; send "\r" }
|
||||
exp_continue
|
||||
}
|
||||
exp_continue
|
||||
}
|
||||
-re {accept} {
|
||||
if {!$answered_bypass} {
|
||||
set answered_bypass 1
|
||||
send_user "\n\[claude-autonomous] accepting the Bypass Permissions disclaimer\n"
|
||||
send "2\r"
|
||||
-re {Do\s+you\s+trust\s+the\s+files} {
|
||||
if {!$answered_trust} {
|
||||
set answered_trust 1
|
||||
send_user "\n\[claude-autonomous] accepting the workspace trust prompt\n"
|
||||
send "1\r"
|
||||
}
|
||||
exp_continue
|
||||
}
|
||||
exp_continue
|
||||
-re {Yes,\s*I\s+accept} {
|
||||
if {!$answered_bypass} {
|
||||
set answered_bypass 1
|
||||
send_user "\n\[claude-autonomous] accepting the Bypass Permissions disclaimer\n"
|
||||
send "2\r"
|
||||
}
|
||||
exp_continue
|
||||
}
|
||||
timeout {
|
||||
# No gate appeared. Stop matching so nothing later in the run can be
|
||||
# answered by accident -- which is exactly what used to happen.
|
||||
}
|
||||
eof { exit }
|
||||
}
|
||||
timeout {
|
||||
# No new gate for a while: the session is up (or never had one). Stop
|
||||
# matching so nothing later in the run can be answered by accident.
|
||||
}
|
||||
eof { exit }
|
||||
}
|
||||
|
||||
# Hand the terminal over for the rest of the run.
|
||||
|
||||
Reference in New Issue
Block a user