From 906bb5e85f65fe3eb8d866d95c568e5b3ab08c26 Mon Sep 17 00:00:00 2001 From: Alexandr Suhinin Date: Thu, 20 Aug 2026 13:06:56 +0300 Subject: [PATCH] fix: send elicitation complete event for device authentication --- src/CodexAcpClient.ts | 25 ++++++++++++++-- src/CodexAcpServer.ts | 22 ++++++++++---- .../CodexACPAgent/CodexAcpClient.test.ts | 29 +++++++++++++++++++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 4e7f2aa4..10e724e7 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -86,6 +86,7 @@ export type UrlElicitationRequest = Omit; + completeElicitation(): Promise; } /** @@ -200,16 +201,34 @@ export class CodexAcpClient { if (loginResponse.type !== "chatgptDeviceCode") { return false; } - const elicitationResponse = await urlElicitationRequester.elicitUrl({ + const elicitationResponsePromise = Promise.resolve(urlElicitationRequester.elicitUrl({ url: loginResponse.verificationUrl, message: `Sign in to ChatGPT and enter this code: ${loginResponse.userCode}`, elicitationId: loginResponse.loginId, - }); - if (!acp.CreateElicitationResponse.isAccept(elicitationResponse)) { + })); + const first = await Promise.race([ + loginCompletedPromise.then(result => ({ + type: "loginCompleted" as const, + result, + })), + elicitationResponsePromise.then(response => ({ + type: "elicitationResponse" as const, + response, + })), + ]); + + if (first.type === "loginCompleted") { + await urlElicitationRequester.completeElicitation(); + return first.result.success; + } + + if (!acp.CreateElicitationResponse.isAccept(first.response)) { await this.codexClient.accountLoginCancel({loginId: loginResponse.loginId}); return false; } + const result = await loginCompletedPromise; + await urlElicitationRequester.completeElicitation(); return result.success; } diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 1c06caa9..cefe45f9 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -858,12 +858,24 @@ export class CodexAcpServer { if (requestId == null || !clientSupportsUrlElicitation(this.clientCapabilities)) { return undefined; } + let elicitationId: string | null = null; return { - elicitUrl: (request) => this.connection.request(acp.methods.client.elicitation.create, { - mode: "url", - requestId, - ...request, - }), + elicitUrl: (request) => { + elicitationId = request.elicitationId; + return this.connection.request(acp.methods.client.elicitation.create, { + mode: "url", + requestId, + ...request, + }); + }, + completeElicitation: async () => { + if (elicitationId === null) { + return; + } + await this.connection.notify(acp.methods.client.elicitation.complete, { + elicitationId, + }); + }, }; } diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 0ba39b70..305982d3 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -254,6 +254,11 @@ describe('ACP server test', { timeout: 40_000 }, () => { url: "https://example.com/device", message: expect.stringContaining("ABCD-1234"), }); + const elicitationComplete = deviceFixture.getAcpConnectionEvents([]) + .find(event => event.method === "completeElicitation"); + expect(elicitationComplete?.args[0]).toEqual({ + elicitationId: "login-1", + }); }); it('should cancel ChatGPT device code login when URL elicitation is declined', async () => { @@ -270,6 +275,30 @@ describe('ACP server test', { timeout: 40_000 }, () => { await expect(codexAcpAgent.authenticate({ methodId: "chat-gpt-device-code" }, 42)) .rejects.toThrow(); expect(cancelSpy).toHaveBeenCalledWith({ loginId: "login-1" }); + expect(deviceFixture.getAcpConnectionEvents([]) + .some(event => event.method === "completeElicitation")) + .toBe(false); + }); + + it('should complete URL elicitation when login finishes before the elicitation response', async () => { + const { deviceFixture, completeLogin, loginCompletedSubscribed } = createDeviceCodeFixture(); + const codexAcpAgent = deviceFixture.getCodexAcpAgent(); + await codexAcpAgent.initialize({ + protocolVersion: 1, + clientCapabilities: { elicitation: { url: {} } }, + }); + deviceFixture.setElicitationResponse(new Promise(() => {})); + + const authPromise = codexAcpAgent.authenticate({ methodId: "chat-gpt-device-code" }, 42); + await vi.waitFor(() => expect(loginCompletedSubscribed()).toBe(true)); + completeLogin(true); + await expect(authPromise).resolves.toEqual({}); + + const elicitationComplete = deviceFixture.getAcpConnectionEvents([]) + .find(event => event.method === "completeElicitation"); + expect(elicitationComplete?.args[0]).toEqual({ + elicitationId: "login-1", + }); }); it('should reject ChatGPT device code auth when the client lacks URL elicitation', async () => {