diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index bb43d50d0..7e05e2b1e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -17,6 +17,10 @@ - Fixed `validateToolArguments` silently accepting JSON-encoded array strings (e.g. `'["a","b"]'`) against `union(string, array)` schemas — providers that double-serialize tool-call arguments (Z.AI / GLM) caused tools like `search` to receive the literal `["a","b"]` as a single path, producing zero matches (single element) or glob parse errors (multi-element). A new pre-validation pass parses JSON-array-shaped strings when the schema explicitly accepts both shapes. ([#1788](https://github.com/can1357/oh-my-pi/issues/1788)) - Fixed Anthropic thinking summaries that arrive wrapped in literal `` tags so advisor/raw transcript dumps do not render nested thinking tags ([#2695](https://github.com/can1357/oh-my-pi/issues/2695)). +### Fixed + +- Fixed Codex browser login issuing credentials for the `opencode` OAuth originator while OMP requests identify as `pi`, which could make the first authenticated Codex request return 401 ([#2696](https://github.com/can1357/oh-my-pi/issues/2696)). + ## [16.0.0] - 2026-06-15 ### Breaking Changes diff --git a/packages/ai/src/registry/oauth/openai-codex.ts b/packages/ai/src/registry/oauth/openai-codex.ts index 5b0876651..73237359a 100644 --- a/packages/ai/src/registry/oauth/openai-codex.ts +++ b/packages/ai/src/registry/oauth/openai-codex.ts @@ -1,6 +1,8 @@ /** * OpenAI Codex (ChatGPT OAuth) flow — browser and device-code flows. */ + +import { OPENAI_HEADER_VALUES } from "@oh-my-pi/pi-catalog/wire/codex"; import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server"; import { generatePKCE } from "./pkce"; import type { OAuthController, OAuthCredentials } from "./types"; @@ -60,6 +62,29 @@ interface PKCE { verifier: string; challenge: string; } +/** Builds the Codex browser OAuth URL used by browser login; exported for auth regression tests. */ +export function createOpenAICodexAuthorizationUrl(args: { + state: string; + redirectUri: string; + challenge: string; + originator?: string; +}): string { + const originator = args.originator?.trim() || OPENAI_HEADER_VALUES.ORIGINATOR_CODEX; + const searchParams = new URLSearchParams({ + response_type: "code", + client_id: CLIENT_ID, + redirect_uri: args.redirectUri, + scope: SCOPE, + code_challenge: args.challenge, + code_challenge_method: "S256", + state: args.state, + id_token_add_organizations: "true", + codex_cli_simplified_flow: "true", + originator, + }); + + return `${AUTHORIZE_URL}?${searchParams.toString()}`; +} class OpenAICodexOAuthFlow extends OAuthCallbackFlow { constructor( @@ -79,20 +104,12 @@ class OpenAICodexOAuthFlow extends OAuthCallbackFlow { } async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> { - const searchParams = new URLSearchParams({ - response_type: "code", - client_id: CLIENT_ID, - redirect_uri: redirectUri, - scope: SCOPE, - code_challenge: this.pkce.challenge, - code_challenge_method: "S256", + const url = createOpenAICodexAuthorizationUrl({ state, - id_token_add_organizations: "true", - codex_cli_simplified_flow: "true", + redirectUri, + challenge: this.pkce.challenge, originator: this.originator, }); - - const url = `${AUTHORIZE_URL}?${searchParams.toString()}`; return { url, instructions: "A browser window should open. Complete login to finish." }; } @@ -153,13 +170,13 @@ async function exchangeCodeForToken(code: string, verifier: string, redirectUri: * Login with OpenAI Codex OAuth */ export type OpenAICodexLoginOptions = OAuthController & { - /** Optional originator value for OpenAI Codex OAuth. Default: "opencode". */ + /** Optional originator value for OpenAI Codex OAuth. Default matches OMP Codex request headers. */ originator?: string; }; export async function loginOpenAICodex(options: OpenAICodexLoginOptions): Promise { const pkce = await generatePKCE(); - const originator = options.originator?.trim() || "opencode"; + const originator = options.originator?.trim() || OPENAI_HEADER_VALUES.ORIGINATOR_CODEX; const flow = new OpenAICodexOAuthFlow(options, pkce, originator); return flow.login(); diff --git a/packages/ai/test/openai-codex.test.ts b/packages/ai/test/openai-codex.test.ts index 58c4b0b5f..2dde67365 100644 --- a/packages/ai/test/openai-codex.test.ts +++ b/packages/ai/test/openai-codex.test.ts @@ -1,13 +1,27 @@ import { describe, expect, it } from "bun:test"; +import { createOpenAICodexAuthorizationUrl } from "@oh-my-pi/pi-ai/oauth/openai-codex"; import { type RequestBody, transformRequestBody } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer"; import { CodexApiError, parseCodexError } from "@oh-my-pi/pi-ai/providers/openai-codex/response-handler"; import { convertOpenAICodexResponsesTools } from "@oh-my-pi/pi-ai/providers/openai-codex-responses"; import type { Tool } from "@oh-my-pi/pi-ai/types"; +import { OPENAI_HEADER_VALUES } from "@oh-my-pi/pi-catalog/wire/codex"; import { createCodexModel } from "./helpers"; const DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant. You help users with coding tasks by reading files, executing commands"; +describe("openai-codex oauth", () => { + it("uses the same default originator for browser login and API requests", () => { + const authUrl = createOpenAICodexAuthorizationUrl({ + state: "state", + redirectUri: "http://localhost:1455/auth/callback", + challenge: "challenge", + }); + + expect(new URL(authUrl).searchParams.get("originator")).toBe(OPENAI_HEADER_VALUES.ORIGINATOR_CODEX); + }); +}); + describe("openai-codex tool schemas", () => { it("adds empty properties to no-argument object parameter schemas", () => { const tools: Tool[] = [