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 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 10:00:36 +00:00
parent 84e70668e8
commit c272ed8a05

View file

@ -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]