diff --git a/src/api/providers/openai-native-codex.ts b/src/api/providers/openai-native-codex.ts index 423b7f2e3f..d6fd6045d4 100644 --- a/src/api/providers/openai-native-codex.ts +++ b/src/api/providers/openai-native-codex.ts @@ -1,6 +1,7 @@ import type { Anthropic } from "@anthropic-ai/sdk" import { promises as fs } from "node:fs" import os from "node:os" +import path from "node:path" import type { ApiHandlerCreateMessageMetadata } from "../index" import { BaseProvider } from "./base-provider" @@ -64,7 +65,6 @@ export class OpenAiNativeCodexHandler extends BaseProvider { protected options: ApiHandlerOptions private chatgptAccessToken!: string private chatgptAccountId?: string - private lastResponseId: string | undefined private lastServiceTier: ServiceTier | undefined // Inline-loaded provider prompt (via esbuild text loader for .md files) @@ -134,18 +134,50 @@ export class OpenAiNativeCodexHandler extends BaseProvider { const expandHome = (p: string) => p.replace(/^~(?=\/|\\|$)/, os.homedir()) const pathToUse = configured && configured.trim() ? configured.trim() : defaultPath const explicitPath = expandHome(pathToUse) + const resolvedPath = path.resolve(explicitPath) + + // Guard file size before reading to prevent loading unexpectedly large files + const MAX_OAUTH_SIZE = 1_000_000 // 1 MB + try { + const stat = await fs.stat(resolvedPath) + if (stat.size > MAX_OAUTH_SIZE) { + throw new Error( + t("common:errors.openaiNativeCodex.oauthFileTooLarge", { + path: resolvedPath, + size: stat.size, + max: MAX_OAUTH_SIZE, + }), + ) + } + } catch (e: any) { + // Surface read failure with localized error (e.g., file missing or inaccessible) + const base = t("common:errors.openaiNativeCodex.oauthReadFailed", { + path: resolvedPath, + error: e?.message || String(e), + }) + throw new Error(base) + } let raw: string try { - raw = await fs.readFile(explicitPath, "utf8") + raw = await fs.readFile(resolvedPath, "utf8") } catch (e: any) { const base = t("common:errors.openaiNativeCodex.oauthReadFailed", { - path: explicitPath, + path: resolvedPath, error: e?.message || String(e), }) - const tip = - " Tip: Authenticate with the Codex CLI to generate auth.json (defaults to ~/.codex/auth.json), then retry." - throw new Error(base + tip) + throw new Error(base) + } + + // Post-read size check using byte length + if (Buffer.byteLength(raw, "utf8") > MAX_OAUTH_SIZE) { + throw new Error( + t("common:errors.openaiNativeCodex.oauthFileTooLarge", { + path: resolvedPath, + size: Buffer.byteLength(raw, "utf8"), + max: MAX_OAUTH_SIZE, + }), + ) } let j: AuthJson @@ -153,12 +185,10 @@ export class OpenAiNativeCodexHandler extends BaseProvider { j = JSON.parse(raw) as AuthJson } catch (e: any) { const base = t("common:errors.openaiNativeCodex.oauthParseFailed", { - path: explicitPath, + path: resolvedPath, error: e?.message || String(e), }) - const tip = - " Tip: Ensure the file is valid JSON or re-authenticate via the Codex CLI to regenerate auth.json." - throw new Error(base + tip) + throw new Error(base) } const tokens: AuthTokens = j?.tokens ?? {} @@ -210,21 +240,10 @@ export class OpenAiNativeCodexHandler extends BaseProvider { settings: this.options, }) - const effort = - (this.options.reasoningEffort as ReasoningEffortWithMinimal | undefined) ?? - (info.reasoningEffort as ReasoningEffortWithMinimal | undefined) - if (effort) { - ;(params.reasoning as any) = { reasoning_effort: effort } - } - + // Reasoning effort is computed by getModelParams based on model + settings return { id: id as string, info, ...params, verbosity: params.verbosity } } - // Expose last response id for conversation continuity consumers (e.g., Task.persistGpt5Metadata) - getLastResponseId(): string | undefined { - return this.lastResponseId - } - override async *createMessage( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], @@ -434,10 +453,7 @@ export class OpenAiNativeCodexHandler extends BaseProvider { } try { const parsed = JSON.parse(data) - // Persist ids/tier when available (parity with openai-native) - if (parsed.response?.id) { - this.lastResponseId = parsed.response.id - } + // Persist tier when available (parity with openai-native) if (parsed.response?.service_tier) { this.lastServiceTier = parsed.response.service_tier as ServiceTier } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index d6a3d998f5..77d57b2859 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -126,6 +126,7 @@ "openaiNativeCodex": { "oauthReadFailed": "Failed to load ChatGPT OAuth credentials at {{path}}: {{error}}. Tip: authenticate with the Codex CLI (e.g., \"codex login\") to create auth.json.", "oauthParseFailed": "Failed to parse ChatGPT OAuth credentials JSON at {{path}}: {{error}}. Tip: ensure the file is valid JSON or re-authenticate with \"codex login\" to regenerate it.", + "oauthFileTooLarge": "OAuth credentials file at {{path}} is too large ({{size}} bytes). Maximum allowed is {{max}} bytes.", "missingAccessToken": "ChatGPT OAuth credentials are missing tokens.access_token.", "httpError": "Codex HTTP {{status}} (req: {{requestId}}) model={{modelId}}: {{message}}", "noResponseBody": "ChatGPT Responses error: No response body", diff --git a/webview-ui/src/components/settings/providers/OpenAiNativeCodex.tsx b/webview-ui/src/components/settings/providers/OpenAiNativeCodex.tsx index 1ab874ad0a..58f189e75c 100644 --- a/webview-ui/src/components/settings/providers/OpenAiNativeCodex.tsx +++ b/webview-ui/src/components/settings/providers/OpenAiNativeCodex.tsx @@ -1,6 +1,7 @@ import React from "react" import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { type ProviderSettings } from "@roo-code/types" +import { useAppTranslation } from "@src/i18n/TranslationContext" interface OpenAiNativeCodexProps { apiConfiguration: ProviderSettings @@ -8,6 +9,7 @@ interface OpenAiNativeCodexProps { } export const OpenAiNativeCodex: React.FC = ({ apiConfiguration, setApiConfigurationField }) => { + const { t } = useAppTranslation() const defaultPath = "~/.codex/auth.json" const handleInputChange = (e: Event | React.FormEvent) => { @@ -24,28 +26,27 @@ export const OpenAiNativeCodex: React.FC = ({ apiConfigu type="text" onInput={handleInputChange} placeholder={defaultPath}> - OAuth Credentials Path +

- Path to your ChatGPT Codex auth.json credentials. Defaults to ~/.codex/auth.json if left empty - (Windows: C:\\Users\\USERNAME\\.codex\\auth.json). + {t("settings:providers.openAiNativeCodex.oauthPathDescription", { defaultPath })}

- ChatGPT Codex uses your ChatGPT web credentials via the official Codex CLI. Authenticate with the - Codex CLI so that auth.json is created. If you use a custom location, set the full file path here. + {t("settings:providers.openAiNativeCodex.oauthCliDescription")}
- After authentication, Roo will read the access token from auth.json and connect to ChatGPT Responses - (Codex). + {t("settings:providers.openAiNativeCodex.oauthConnectDescription")}
- Learn more about ChatGPT + {t("settings:providers.openAiNativeCodex.learnMoreLinkText")} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 88be222924..99d1b056a9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -449,6 +449,13 @@ "authenticatedMessage": "Securely authenticated through your Roo Code Cloud account.", "connectButton": "Connect to Roo Code Cloud" }, + "openAiNativeCodex": { + "oauthPathLabel": "OAuth Credentials Path", + "oauthPathDescription": "Path to your ChatGPT Codex auth.json credentials. Defaults to {{defaultPath}} if left empty (Windows: C:\\\\Users\\\\USERNAME\\\\.codex\\\\auth.json).", + "oauthCliDescription": "ChatGPT Codex uses your ChatGPT web credentials via the official Codex CLI. Authenticate with the Codex CLI so that auth.json is created. If you use a custom location, set the full file path here.", + "oauthConnectDescription": "After authentication, Roo will read the access token from auth.json and connect to ChatGPT Responses (Codex).", + "learnMoreLinkText": "Learn more about ChatGPT" + }, "openRouter": { "providerRouting": { "title": "OpenRouter Provider Routing",