From 9ad083102aa9afd483dbb5e0da53e69f2902c6a6 Mon Sep 17 00:00:00 2001 From: Lucas Meijer Date: Tue, 6 Oct 2026 09:27:40 +0200 Subject: [PATCH] feat(ai): let apps name themselves in OpenAI logins (#10433) LoginOptions.agentName replaces pi's name in the Sign in with ChatGPT agent_name_hint and the Codex browser login originator. --- packages/ai/src/auth/oauth/openai-chatgpt.ts | 2 +- packages/ai/src/auth/oauth/openai-codex.ts | 13 +++++---- packages/ai/src/auth/types.ts | 5 ++++ packages/ai/test/openai-chatgpt-oauth.test.ts | 17 +++++++++++ packages/ai/test/openai-codex-oauth.test.ts | 28 +++++++++++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/ai/src/auth/oauth/openai-chatgpt.ts b/packages/ai/src/auth/oauth/openai-chatgpt.ts index c6b049139..65e037135 100644 --- a/packages/ai/src/auth/oauth/openai-chatgpt.ts +++ b/packages/ai/src/auth/oauth/openai-chatgpt.ts @@ -250,7 +250,7 @@ async function loginOpenAIChatGPT( const authorizationUrl = new URL(AUTHORIZE_URL); authorizationUrl.search = new URLSearchParams({ client_id: DYNAMIC_CLIENT_ID, - agent_name_hint: AGENT_NAME_HINT, + agent_name_hint: options?.agentName ?? AGENT_NAME_HINT, ext_agent_host_id: hostId, response_type: "code", redirect_uri: REDIRECT_URI, diff --git a/packages/ai/src/auth/oauth/openai-codex.ts b/packages/ai/src/auth/oauth/openai-codex.ts index d2917f1e1..860ea7554 100644 --- a/packages/ai/src/auth/oauth/openai-codex.ts +++ b/packages/ai/src/auth/oauth/openai-codex.ts @@ -14,7 +14,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version } import { getProviderEnvValue } from "../../utils/provider-env.ts"; -import type { OAuthAuth, OAuthCredential, ProviderAuthInteraction } from "../types.ts"; +import type { LoginOptions, OAuthAuth, OAuthCredential, ProviderAuthInteraction } from "../types.ts"; import { startOAuthCallbackServer, waitForCallbackOrManualInput } from "./callback-server.ts"; import { pollOAuthDeviceCodeFlow } from "./device-code.ts"; import { generatePKCE } from "./pkce.ts"; @@ -356,8 +356,11 @@ async function loginOpenAICodexDeviceCode(interaction: ProviderAuthInteraction): ); } -async function loginOpenAICodex(interaction: ProviderAuthInteraction): Promise { - const { verifier, state, url } = await createAuthorizationFlow(); +async function loginOpenAICodex( + interaction: ProviderAuthInteraction, + options?: LoginOptions, +): Promise { + const { verifier, state, url } = await createAuthorizationFlow(options?.agentName); // Port 1455 is shared with the Codex CLI; when it is taken, fall back to the pasted redirect URL. const callback = await startOAuthCallbackServer({ providerName: "OpenAI", @@ -407,7 +410,7 @@ export const openaiCodexOAuth: OAuthAuth = { name: "OpenAI (ChatGPT Plus/Pro)", isSubscription: true, - async login(interaction) { + async login(interaction, options) { const method = await interaction.prompt({ type: "select", message: "Select OpenAI Codex login method:", @@ -424,7 +427,7 @@ export const openaiCodexOAuth: OAuthAuth = { throw new Error(`Unknown OpenAI Codex login method: ${method}`); } - return loginOpenAICodex(interaction); + return loginOpenAICodex(interaction, options); }, refresh: (credential, signal) => refreshOpenAICodexToken(credential.refresh, signal), diff --git a/packages/ai/src/auth/types.ts b/packages/ai/src/auth/types.ts index ce803b716..60fe4ff85 100644 --- a/packages/ai/src/auth/types.ts +++ b/packages/ai/src/auth/types.ts @@ -206,6 +206,11 @@ export interface LoginOptions { * the ID on first use and must return the same ID on every later call. */ getDeviceId?: () => string; + /** + * Name this app introduces itself with during login, e.g. OpenAI's agent name hint and + * Codex originator. Defaults to pi's own name. + */ + agentName?: string; } /** diff --git a/packages/ai/test/openai-chatgpt-oauth.test.ts b/packages/ai/test/openai-chatgpt-oauth.test.ts index 64e60da02..35ba33aed 100644 --- a/packages/ai/test/openai-chatgpt-oauth.test.ts +++ b/packages/ai/test/openai-chatgpt-oauth.test.ts @@ -108,6 +108,23 @@ describe("OpenAI ChatGPT OAuth", () => { }); }); + it("uses the app's agent name as the name hint", async () => { + let authorizeUrl: URL | undefined; + stubTokenEndpoint(tokenResponse()); + + await openaiChatGPTOAuth.login( + loginInteraction({ + callbackClientId: "oaiapp_issued", + onAuthorize: (url) => { + authorizeUrl = url; + }, + }), + { getDeviceId: () => DEVICE_ID, agentName: "my-app" }, + ); + + expect(authorizeUrl?.searchParams.get("agent_name_hint")).toBe("my-app"); + }); + it("rejects registration without an issued client ID", async () => { const fetchMock = stubTokenEndpoint(tokenResponse()); diff --git a/packages/ai/test/openai-codex-oauth.test.ts b/packages/ai/test/openai-codex-oauth.test.ts index 16ec6c76f..269e6b85b 100644 --- a/packages/ai/test/openai-codex-oauth.test.ts +++ b/packages/ai/test/openai-codex-oauth.test.ts @@ -485,6 +485,34 @@ describe("OpenAI Codex OAuth", () => { expect(consoleError).not.toHaveBeenCalled(); }); + it("uses the app's agent name as the browser login originator", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + jsonResponse({ access_token: createAccessToken("acct"), refresh_token: "refresh", expires_in: 3600 }), + ), + ); + + let authUrl = ""; + await openaiCodexOAuth.login( + { + signal: neverAbortedSignal, + notify: (event) => { + if (event.type === "auth_url") authUrl = event.url; + }, + prompt: async (prompt) => { + if (prompt.type === "select") return "browser"; + if (prompt.type !== "manual_code") throw new Error(`Unexpected prompt: ${prompt.type}`); + const state = new URL(authUrl).searchParams.get("state"); + return `http://localhost:1455/auth/callback?code=pasted-code&state=${state}`; + }, + }, + { agentName: "my-app" }, + ); + + expect(new URL(authUrl).searchParams.get("originator")).toBe("my-app"); + }); + it("falls back to the pasted redirect URL when the fixed callback port is taken", async () => { // Port 1455 is registered with OpenAI; the Codex CLI may hold it. Occupy it unless it already is. const blocker = createServer();