From b95e02de3ace5d8e1e0a0295c63e9e23bb439e87 Mon Sep 17 00:00:00 2001 From: Agents Agent Date: Thu, 25 Jun 2026 16:57:48 +0200 Subject: [PATCH] fix(agents-login): finalize Claude capture on parsed token, not just success line claude setup-token prints the OAuth token after the redirect URL is pasted but can keep the PTY open without printing a recognized success line, so the session stayed awaiting_url forever and never captured or POSTed to agents-api. The worker now treats a parsed Claude OAuth token as the completion signal and immediately finalizes, captures, and posts; post-submission CLI failure output now transitions the session to failed instead of being ignored. Adds a regression test. --- services/agents-login/src/worker/session.ts | 5 +++-- services/agents-login/test/session.test.ts | 24 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/services/agents-login/src/worker/session.ts b/services/agents-login/src/worker/session.ts index adabda43..e2689f8a 100644 --- a/services/agents-login/src/worker/session.ts +++ b/services/agents-login/src/worker/session.ts @@ -129,7 +129,7 @@ export class LoginSession { return } - if (detectFailure(this.buffer) && this.phase === 'starting') { + if (detectFailure(this.buffer) && (this.phase === 'starting' || this.redirectSubmitted)) { this.fail('CLI reported a login failure') return } @@ -157,7 +157,8 @@ export class LoginSession { } } - if (detectSuccess(this.provider, this.buffer)) { + const claudeTokenCaptured = this.provider === 'claude' && this.redirectSubmitted && parseClaudeToken(this.buffer) + if (claudeTokenCaptured || detectSuccess(this.provider, this.buffer)) { void this.finalize(updatedBy) } } diff --git a/services/agents-login/test/session.test.ts b/services/agents-login/test/session.test.ts index 2aec3c3c..51a5a9c9 100644 --- a/services/agents-login/test/session.test.ts +++ b/services/agents-login/test/session.test.ts @@ -152,6 +152,30 @@ describe('LoginSession state machine', () => { ]) }) + it('finalizes Claude promptly when setup-token prints the OAuth token after paste-back but stays open', async () => { + // Production regression: the worker waited for a success phrase or process + // exit, so a CLI that printed the token and kept the PTY open never posted. + const token = 'sk-ant-oat01-NoExitToken1234567890_-abcXYZ' + const { deps: d } = deps(() => [ + { type: 'emit', data: 'Open https://claude.com/cai/oauth/authorize?code=true\r\n' }, + { + type: 'expectStdin', + match: () => true, + then: [{ type: 'emit', data: `CLAUDE_CODE_OAUTH_TOKEN=${token}\r\n` }], + }, + ]) + const mgr = new SessionManager(d) + const started = mgr.start('claude', 'alice') + await tick() + + expect(mgr.submitRedirectUrl(started.id, 'paste-code-xyz').ok).toBe(true) + + expect(await waitPhase(mgr, started.id, ['succeeded', 'failed'], 250)).toBe('succeeded') + expect(agentsApi.posts).toEqual([ + { userId: 'alice', provider: 'CLAUDE', payload: { oauth_token: token } }, + ]) + }) + it('drives the Codex device flow with no paste-back', async () => { writeFileSync(join(codexHome, 'auth.json'), '{"tokens":{}}') writeFileSync(join(codexHome, 'config.toml'), 'model="x"\n')