Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion services/agents-login/src/worker/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 21 additions & 1 deletion services/agents-login/src/worker/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions services/agents-login/test/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
38 changes: 37 additions & 1 deletion services/agents-login/test/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down