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.
This commit is contained in:
Lucas Meijer 2026-10-06 09:27:40 +02:00 • committed by GitHub
parent f6127a1bf9
commit 9ad083102a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 59 additions and 6 deletions

View file

@ -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,

View file

@ -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<OAuthCredential> {
const { verifier, state, url } = await createAuthorizationFlow();
async function loginOpenAICodex(
interaction: ProviderAuthInteraction,
options?: LoginOptions,
): Promise<OAuthCredential> {
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),

View file

@ -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;
}
/**

View file

@ -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());

View file

@ -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();