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
27 changes: 19 additions & 8 deletions src/agent/chatgpt-auth-state-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,12 +311,8 @@ export class ChatGptAuthStateMachine implements ChatGptSetupController {
this.#status = { state: "not_connected" };
}
await this.refreshCapabilities();
if (
this.#accountState === "connected" &&
this.#status.state !== "starting" &&
this.#status.state !== "awaiting_authorization"
) {
this.#status = { state: "connected" };
if (this.#accountState === "connected") {
await this.#transitionToConnected();
}
return;
}
Expand Down Expand Up @@ -364,10 +360,25 @@ export class ChatGptAuthStateMachine implements ChatGptSetupController {
try {
await this.refreshCapabilities();
if (this.#accountState !== "connected") {
this.#loginId = undefined;
this.#status = { state: "failed", code: "CHATGPT_LOGIN_FAILED" };
// App Server emits login completion before reloading its account.
// Keep waiting for the authoritative account/updated notification.
return;
}
await this.#transitionToConnected();
} catch {
this.#loginId = undefined;
this.#status = {
state: "failed",
code: "CHATGPT_CREDENTIAL_SAVE_FAILED",
};
}
}

async #transitionToConnected(): Promise<void> {
if (this.#status.state === "connected") {
return;
}
try {
await validateAndRestrictCodexAuthFile(this.#codexHome);
} catch {
this.#loginId = undefined;
Expand Down
31 changes: 29 additions & 2 deletions test/unit/codex-app-server-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ async function temporaryCodexHome(): Promise<string> {
}

describe("Codex App Server ChatGPT authentication", () => {
it("performs the pinned App Server handshake and exposes only the device ceremony", async () => {
it("waits for the pinned App Server account update before completing device login", async () => {
const codexHome = await temporaryCodexHome();
const connection = new FakeConnection(codexHome);
const connected = vi.fn();
Expand Down Expand Up @@ -180,20 +180,47 @@ describe("Codex App Server ChatGPT authentication", () => {
});

await writeFile(join(codexHome, "auth.json"), "fixture", { mode: 0o600 });
connection.connected = true;
connection.emit("account/login/completed", {
loginId: "3ea32ef5-f9b0-4d0e-b59c-d9838db91f92",
success: true,
error: null,
onboardingEntrypoint: null,
});
await vi.waitFor(() =>
expect(
connection.requests.filter(
(request) => request.method === "account/read",
),
).toHaveLength(2),
);
expect(auth.status()).toMatchObject({ state: "awaiting_authorization" });
expect(connected).not.toHaveBeenCalled();

connection.connected = true;
connection.emit("account/updated", {
authMode: "chatgpt",
planType: "plus",
});
await vi.waitFor(() => expect(auth.status()).toEqual({ state: "connected" }));
expect(connected).toHaveBeenCalledOnce();
expect(auth.capabilities()).toMatchObject({
state: "available",
planType: "plus",
models: [{ id: "gpt-5.6-luna" }],
});

connection.emit("account/updated", {
authMode: "chatgpt",
planType: "plus",
});
await vi.waitFor(() =>
expect(
connection.requests.filter(
(request) => request.method === "account/read",
),
).toHaveLength(4),
);
expect(connected).toHaveBeenCalledOnce();
await auth.close();
expect(connection.closed).toBe(true);
});
Expand Down
Loading