From 58b86cb9e6d8635e831c721ce4d5e8dfddb8ccfc Mon Sep 17 00:00:00 2001 From: Agents Agent Date: Thu, 25 Jun 2026 18:23:44 +0200 Subject: [PATCH] fix(agents-login): match the space-less paste prompt so the code is actually written The login hung after the user submitted the auth code because the worker never wrote it to the PTY. claude setup-token renders 'Paste code here if prompted >' using cursor-position ANSI (no literal spaces), so after stripAnsi the buffer is 'Pastecodehereifprompted>'. detectClaudeCodePrompt required whitespace between words (\s+), so it never matched, the paste-prompt gate never fired, and the queued code was never written. - detectClaudeCodePrompt now tolerates zero whitespace between words (\s*), matching both the spaced and cursor-positioned prompt forms. - submitRedirectUrl / maybeSubmitPendingClaudeCode fail loudly (phase failed) if there is no live PTY handle, instead of silently no-oping. - Tests cover the real cursor-positioned prompt bytes and the missing-process failure. --- services/agents-login/src/worker/parse.ts | 2 +- services/agents-login/src/worker/session.ts | 22 +++++++++++- services/agents-login/test/parse.test.ts | 3 ++ services/agents-login/test/session.test.ts | 38 ++++++++++++++++++++- 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/services/agents-login/src/worker/parse.ts b/services/agents-login/src/worker/parse.ts index a0261df8..b7641003 100644 --- a/services/agents-login/src/worker/parse.ts +++ b/services/agents-login/src/worker/parse.ts @@ -69,7 +69,7 @@ export function parseClaude(buffer: string): ClaudeParse { } export function detectClaudeCodePrompt(buffer: string): boolean { - return /paste\s+code\s+here\s+if\s+prompted\s*>/i.test(stripAnsi(buffer)) + return /paste\s*code\s*here\s*if\s*prompted\s*>/i.test(stripAnsi(buffer)) } export interface ClaudeRedirectCode { diff --git a/services/agents-login/src/worker/session.ts b/services/agents-login/src/worker/session.ts index 1e760819..497c265d 100644 --- a/services/agents-login/src/worker/session.ts +++ b/services/agents-login/src/worker/session.ts @@ -242,7 +242,15 @@ export class LoginSession { } private maybeSubmitPendingClaudeCode(): void { - if (!this.pendingClaudeCode || this.redirectSubmitted || !this.proc || !detectClaudeCodePrompt(this.buffer)) { + if (!this.pendingClaudeCode || this.redirectSubmitted || !detectClaudeCodePrompt(this.buffer)) { + return + } + if (!this.proc) { + this.deps.logger.error('Claude authorization code submission failed: PTY process handle missing', { + sessionId: this.id, + provider: this.provider, + }) + this.fail('cannot submit Claude authorization code: PTY process handle missing') return } const code = this.pendingClaudeCode @@ -306,6 +314,18 @@ export class LoginSession { if (!parsed) { return { ok: false, error: 'authorization code is required' } } + if (!this.proc) { + const reason = 'cannot submit Claude authorization code: PTY process handle missing' + this.deps.logger.error('Claude redirect submission failed: PTY process handle missing', { + sessionId: this.id, + provider: this.provider, + inputBytes: url.trim().length, + codeBytes: parsed.code.length, + inputSource: parsed.source, + }) + this.fail(reason) + return { ok: false, error: reason } + } this.deps.logger.info('Claude redirect submission received', { sessionId: this.id, provider: this.provider, diff --git a/services/agents-login/test/parse.test.ts b/services/agents-login/test/parse.test.ts index 5a639411..473c6037 100644 --- a/services/agents-login/test/parse.test.ts +++ b/services/agents-login/test/parse.test.ts @@ -57,6 +57,9 @@ describe('PTY output parsing', () => { }) it('detects the Claude setup-token code prompt', () => { + expect(detectClaudeCodePrompt('\x1b[2GPaste\x1b[8Gcode\x1b[13Ghere\x1b[18Gif\x1b[21Gprompted\x1b[30G>')).toBe( + true, + ) expect(detectClaudeCodePrompt('Paste code here if prompted >')).toBe(true) expect(detectClaudeCodePrompt('Opening browser to sign in...')).toBe(false) }) diff --git a/services/agents-login/test/session.test.ts b/services/agents-login/test/session.test.ts index a24e9775..47fecb40 100644 --- a/services/agents-login/test/session.test.ts +++ b/services/agents-login/test/session.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { SessionManager, type SessionDeps } from '../src/worker/session.js' +import { LoginSession, SessionManager, type SessionDeps } from '../src/worker/session.js' import { NoopLeaseLock } from '../src/worker/lease.js' import { createLogger, type Logger } from '../src/shared/log.js' import { fakeSpawner, type Action } from './helpers/fakePty.js' @@ -191,6 +191,42 @@ describe('LoginSession state machine', () => { expect(exitCbs.length).toBe(1) }) + it('writes Claude authorization codes to the live PTY and fails if the handle is missing', async () => { + const { deps: liveDeps, instances } = deps(() => [ + { type: 'emit', data: 'Visit https://claude.ai/oauth/authorize?code=1\r\nPaste code here if prompted >' }, + { type: 'expectStdin', match: () => true, then: [] }, + ]) + const live = new LoginSession('claude', liveDeps) + live.start('alice') + await tick() + + expect( + live.submitRedirectUrl('https://platform.claude.com/oauth/code/callback?code=live-code-123&state=state-1').ok, + ).toBe(true) + expect(instances[0].writes).toEqual(['live-code-123\r']) + + const logs: LogEntry[] = [] + const { deps: missingDeps } = deps( + () => [{ type: 'emit', data: 'Visit https://claude.ai/oauth/authorize?code=1\r\nPaste code here if prompted >' }], + new NoopLeaseLock(), + memoryLogger(logs), + ) + const missing = new LoginSession('claude', missingDeps) + missing.start('alice') + await tick() + ;(missing as unknown as { proc?: undefined }).proc = undefined + + const result = missing.submitRedirectUrl('https://platform.claude.com/oauth/code/callback?code=lost-code-456') + + expect(result).toEqual({ + ok: false, + error: 'cannot submit Claude authorization code: PTY process handle missing', + }) + expect(missing.status().phase).toBe('failed') + expect(missing.status().error).toBe('cannot submit Claude authorization code: PTY process handle missing') + expect(logs.some((entry) => entry.level === 'error' && entry.msg.includes('PTY process handle missing'))).toBe(true) + }) + it('captures the setup-token OAuth token from stdout when no credentials file is written', async () => { // No .credentials.json is created — setup-token only prints the token. const token = 'sk-ant-oat01-StdoutOnlyToken1234567890_-abcXYZ'