diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c37112cee6..b89cd49f60d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai - Telegram/media downloads: let Telegram media fetches trust an operator-configured explicit proxy for target DNS resolution after hostname-policy checks, so proxy-backed installs stop failing `could not download media` on Bot API file downloads after the DNS-pinning regression. (#66245) Thanks @dawei41468 and @vincentkoc. - Browser: keep loopback CDP readiness checks reachable under strict SSRF defaults so OpenClaw can reconnect to locally started managed Chrome. (#66354) Thanks @hxy91819. - Agents/context engine: compact engine-owned sessions from the first tool-loop delta and preserve ingest fallback when `afterTurn` is absent, so long-running tool loops can stay bounded without dropping engine state. (#63555) Thanks @Bikkies. +- OpenAI Codex/auth: keep malformed Codex CLI auth-file diagnostics on the debug logger instead of stdout so interactive command output stays clean while auth read failures remain traceable. (#66451) Thanks @SimbaKingjoe. - Discord/native commands: return the real status card for native `/status` interactions instead of falling through to the synthetic `✅ Done.` ack when the generic dispatcher produces no visible reply. (#54629) Thanks @tkozzer and @vincentkoc. - Hooks/Ollama: let LLM-backed session-memory slug generation honor an explicit `agents.defaults.timeoutSeconds` override instead of always aborting after 15 seconds, so slow local Ollama runs stop silently dropping back to generic filenames. (#66237) Thanks @dmak and @vincentkoc. - Media/transcription: remap `.aac` filenames to `.m4a` for OpenAI-compatible audio uploads so AAC voice notes stop failing MIME-sensitive transcription endpoints. (#66446) Thanks @ben-z. diff --git a/extensions/openai/index.test.ts b/extensions/openai/index.test.ts index 61be535cfb8..065ab53dce5 100644 --- a/extensions/openai/index.test.ts +++ b/extensions/openai/index.test.ts @@ -22,9 +22,15 @@ const runtimeMocks = vi.hoisted(() => ({ refreshOpenAICodexToken: vi.fn(), })); -vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ - ensureGlobalUndiciEnvProxyDispatcher: runtimeMocks.ensureGlobalUndiciEnvProxyDispatcher, -})); +vi.mock("openclaw/plugin-sdk/runtime-env", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/runtime-env", + ); + return { + ...actual, + ensureGlobalUndiciEnvProxyDispatcher: runtimeMocks.ensureGlobalUndiciEnvProxyDispatcher, + }; +}); vi.mock("@mariozechner/pi-ai/oauth", async () => { const actual = await vi.importActual( diff --git a/extensions/openai/openai-codex-cli-auth.test.ts b/extensions/openai/openai-codex-cli-auth.test.ts index b941ca2121a..a8bcd103fad 100644 --- a/extensions/openai/openai-codex-cli-auth.test.ts +++ b/extensions/openai/openai-codex-cli-auth.test.ts @@ -1,5 +1,16 @@ import fs from "node:fs"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const runtimeMocks = vi.hoisted(() => ({ + debug: vi.fn(), +})); + +vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ + createSubsystemLogger: () => ({ + debug: runtimeMocks.debug, + }), +})); + import { OPENAI_CODEX_DEFAULT_PROFILE_ID, readOpenAICodexCliOAuthProfile, @@ -12,6 +23,10 @@ function buildJwt(payload: Record) { } describe("readOpenAICodexCliOAuthProfile", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + afterEach(() => { vi.restoreAllMocks(); }); @@ -80,4 +95,54 @@ describe("readOpenAICodexCliOAuthProfile", () => { expect(parsed).toBeNull(); }); + + it("returns null without logging when the Codex CLI auth file is missing", () => { + const error = Object.assign(new Error("missing"), { + code: "ENOENT", + }); + vi.spyOn(fs, "readFileSync").mockImplementation(() => { + throw error; + }); + + const parsed = readOpenAICodexCliOAuthProfile({ + store: { version: 1, profiles: {} }, + }); + + expect(parsed).toBeNull(); + expect(runtimeMocks.debug).not.toHaveBeenCalled(); + }); + + it("logs a sanitized code for invalid auth JSON", () => { + vi.spyOn(fs, "readFileSync").mockReturnValue("{"); + + const parsed = readOpenAICodexCliOAuthProfile({ + store: { version: 1, profiles: {} }, + }); + + expect(parsed).toBeNull(); + expect(runtimeMocks.debug).toHaveBeenCalledWith( + "Failed to read Codex CLI auth file (code=INVALID_JSON)", + ); + }); + + it("does not leak auth file paths in debug logs for filesystem failures", () => { + const error = Object.assign( + new Error("EACCES: permission denied, open '/Users/alice/.codex/auth.json'"), + { + code: "EACCES", + }, + ); + vi.spyOn(fs, "readFileSync").mockImplementation(() => { + throw error; + }); + + const parsed = readOpenAICodexCliOAuthProfile({ + store: { version: 1, profiles: {} }, + }); + + expect(parsed).toBeNull(); + expect(runtimeMocks.debug).toHaveBeenCalledWith( + "Failed to read Codex CLI auth file (code=EACCES)", + ); + }); }); diff --git a/extensions/openai/openai-codex-cli-auth.ts b/extensions/openai/openai-codex-cli-auth.ts index 289e2bef017..314d7560063 100644 --- a/extensions/openai/openai-codex-cli-auth.ts +++ b/extensions/openai/openai-codex-cli-auth.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import type { AuthProfileStore, OAuthCredential } from "openclaw/plugin-sdk/provider-auth"; import { resolveRequiredHomeDir } from "openclaw/plugin-sdk/provider-auth"; +import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; import { resolveCodexAccessTokenExpiry, resolveCodexAuthIdentity, @@ -9,6 +10,7 @@ import { import { trimNonEmptyString } from "./openai-codex-shared.js"; const PROVIDER_ID = "openai-codex"; +const log = createSubsystemLogger("openai/codex-cli-auth"); export const CODEX_CLI_PROFILE_ID = `${PROVIDER_ID}:codex-cli`; export const OPENAI_CODEX_DEFAULT_PROFILE_ID = `${PROVIDER_ID}:default`; @@ -42,7 +44,19 @@ function readCodexCliAuthFile(env: NodeJS.ProcessEnv): CodexCliAuthFile | null { const raw = fs.readFileSync(authPath, "utf8"); const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" ? (parsed as CodexCliAuthFile) : null; - } catch { + } catch (error) { + const code = + error instanceof SyntaxError + ? "INVALID_JSON" + : error instanceof Error && "code" in error + ? (error as NodeJS.ErrnoException).code + : undefined; + if (code === "ENOENT") { + return null; + } + log.debug( + `Failed to read Codex CLI auth file (code=${typeof code === "string" ? code : "UNKNOWN"})`, + ); return null; } }