From c272ed8a0567a786cbd832410a78e6cd2fb76756 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 14 Jun 2026 10:00:36 +0000 Subject: [PATCH] join@: require a PTY so onboarding can't hang handleJoin reads the email and verification code interactively, but the router deliberately skipped the active-PTY guard for join@ on the wrong assumption that it "prints and disconnects." A client without a controlling tty (ssh delegating prompts to ssh-askpass) gets no PTY, so the email prompt blocked forever after the account banner. Guard handleJoin for a PTY and emit a "reconnect with ssh -t" hint instead of hanging; fix the misleading router comment. Co-Authored-By: Claude Opus 4.8 --- cmd/agentbbs/main.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cmd/agentbbs/main.go b/cmd/agentbbs/main.go index 261bfb1..a329c56 100644 --- a/cmd/agentbbs/main.go +++ b/cmd/agentbbs/main.go @@ -194,8 +194,9 @@ func main() { } // router dispatches a session by username (PRD §4.4 + pods addendum). -// The active-PTY guard applies to hub sessions only: join@ must work without -// a terminal (it prints and disconnects), and pod@ checks its PTY itself. +// The active-PTY guard applies to hub sessions only; join@ and pod@ check their +// own PTY (both are interactive) so they can return a tailored hint instead of +// activeterm's opaque rejection. func (a *app) router() wish.Middleware { btMw := bm.Middleware(a.teaHandler) adminMw := bm.Middleware(a.adminTeaHandler) @@ -291,6 +292,15 @@ func (a *app) handleJoin(s ssh.Session) { _ = s.Exit(1) return } + // Onboarding reads an email and a verification code interactively, so it + // needs a terminal. Without a PTY the prompts would block forever (e.g. ssh + // launched with no controlling tty, which delegates prompts to ssh-askpass). + // Fail fast with a hint instead of hanging. + if _, _, hasPty := s.Pty(); !hasPty { + wish.Println(s, "join@ is interactive — reconnect with a terminal: ssh -t join@"+a.host) + _ = s.Exit(1) + return + } u, found, err := a.st.UserByFingerprint(fp) if err == nil && !found { name := "member-" + strings.ToLower(strings.TrimPrefix(fp, "SHA256:"))[:8]