From 490b2f881cea044cde3c9ba420fa4e38200141dd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 27 Mar 2026 04:06:20 +0000 Subject: [PATCH] fix(ci): restore codex oauth refresh fallback --- .../openai/openai-codex-provider.runtime.ts | 12 +- extensions/openai/openai-codex-provider.ts | 11 +- extensions/openai/openai-provider.test.ts | 8 +- src/config/config-misc.test.ts | 3 +- .../contracts/runtime.contract.test.ts | 104 +++++++++++------- 5 files changed, 84 insertions(+), 54 deletions(-) diff --git a/extensions/openai/openai-codex-provider.runtime.ts b/extensions/openai/openai-codex-provider.runtime.ts index c79af99d6ec..ab5779071f1 100644 --- a/extensions/openai/openai-codex-provider.runtime.ts +++ b/extensions/openai/openai-codex-provider.runtime.ts @@ -1,4 +1,7 @@ -import { getOAuthApiKey as getOAuthApiKeyFromPi } from "@mariozechner/pi-ai/oauth"; +import { + getOAuthApiKey as getOAuthApiKeyFromPi, + refreshOpenAICodexToken as refreshOpenAICodexTokenFromPi, +} from "@mariozechner/pi-ai/oauth"; import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/infra-runtime"; export async function getOAuthApiKey( @@ -7,3 +10,10 @@ export async function getOAuthApiKey( ensureGlobalUndiciEnvProxyDispatcher(); return await getOAuthApiKeyFromPi(...args); } + +export async function refreshOpenAICodexToken( + ...args: Parameters +): Promise>> { + ensureGlobalUndiciEnvProxyDispatcher(); + return await refreshOpenAICodexTokenFromPi(...args); +} diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index db7b9cef1f5..b1c723471fd 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -139,16 +139,11 @@ function resolveCodexForwardCompatModel( async function refreshOpenAICodexOAuthCredential(cred: OAuthCredential) { try { - const { getOAuthApiKey } = await import("./openai-codex-provider.runtime.js"); - const refreshed = await getOAuthApiKey("openai-codex", { - "openai-codex": cred, - }); - if (!refreshed) { - throw new Error("OpenAI Codex OAuth refresh returned no credentials."); - } + const { refreshOpenAICodexToken } = await import("./openai-codex-provider.runtime.js"); + const refreshed = await refreshOpenAICodexToken(cred.refresh); return { ...cred, - ...refreshed.newCredentials, + ...refreshed, type: "oauth" as const, provider: PROVIDER_ID, email: cred.email, diff --git a/extensions/openai/openai-provider.test.ts b/extensions/openai/openai-provider.test.ts index 979499d09a1..edcf404e5be 100644 --- a/extensions/openai/openai-provider.test.ts +++ b/extensions/openai/openai-provider.test.ts @@ -3,10 +3,10 @@ import { describe, expect, it, vi } from "vitest"; import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js"; import { buildOpenAIProvider } from "./openai-provider.js"; -const getCodexOAuthApiKeyMock = vi.hoisted(() => vi.fn()); +const refreshOpenAICodexTokenMock = vi.hoisted(() => vi.fn()); vi.mock("./openai-codex-provider.runtime.js", () => ({ - getOAuthApiKey: getCodexOAuthApiKeyMock, + refreshOpenAICodexToken: refreshOpenAICodexTokenMock, })); const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? ""; @@ -239,8 +239,8 @@ describe("buildOpenAIProvider", () => { expires: Date.now() - 60_000, }; - getCodexOAuthApiKeyMock.mockReset(); - getCodexOAuthApiKeyMock.mockRejectedValueOnce( + refreshOpenAICodexTokenMock.mockReset(); + refreshOpenAICodexTokenMock.mockRejectedValueOnce( new Error("Failed to extract accountId from token"), ); diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index 8de11cdb086..ca88cff8867 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -476,8 +476,7 @@ describe("config strict validation", () => { it("flags legacy config entries without auto-migrating", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { - agents: { list: [{ id: "pi" }] }, - routing: { allowFrom: ["+15555550123"] }, + memorySearch: { provider: "local", fallback: "none" }, }); const snap = await readConfigFileSnapshot(); diff --git a/src/plugins/contracts/runtime.contract.test.ts b/src/plugins/contracts/runtime.contract.test.ts index 09b36576ff4..7124d92db84 100644 --- a/src/plugins/contracts/runtime.contract.test.ts +++ b/src/plugins/contracts/runtime.contract.test.ts @@ -9,6 +9,7 @@ import { requireProviderContractProvider as requireBundledProviderContractProvid const CONTRACT_SETUP_TIMEOUT_MS = 300_000; const getOAuthApiKeyMock = vi.hoisted(() => vi.fn()); +const refreshOpenAICodexTokenMock = vi.hoisted(() => vi.fn()); const getOAuthProvidersMock = vi.hoisted(() => vi.fn(() => [ { id: "anthropic", envApiKey: "ANTHROPIC_API_KEY", oauthTokenEnv: "ANTHROPIC_OAUTH_TOKEN" }, // pragma: allowlist secret @@ -25,11 +26,12 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => { ...actual, getOAuthApiKey: getOAuthApiKeyMock, getOAuthProviders: getOAuthProvidersMock, + refreshOpenAICodexToken: refreshOpenAICodexTokenMock, }; }); -vi.mock("../../../extensions/openai/src/openai-codex-provider.runtime.js", () => ({ - getOAuthApiKey: getOAuthApiKeyMock, +vi.mock("../../../extensions/openai/openai-codex-provider.runtime.js", () => ({ + refreshOpenAICodexToken: refreshOpenAICodexTokenMock, })); function createModel(overrides: Partial & Pick) { @@ -55,34 +57,39 @@ describe("provider runtime contract", () => { beforeEach(() => { getOAuthApiKeyMock.mockReset(); getOAuthProvidersMock.mockClear(); + refreshOpenAICodexTokenMock.mockReset(); }, CONTRACT_SETUP_TIMEOUT_MS); describe("anthropic", () => { - it("owns anthropic 4.6 forward-compat resolution", () => { - const provider = requireProviderContractProvider("anthropic"); - const model = provider.resolveDynamicModel?.({ - provider: "anthropic", - modelId: "claude-sonnet-4.6-20260219", - modelRegistry: { - find: (_provider: string, id: string) => - id === "claude-sonnet-4.5-20260219" - ? createModel({ - id: id, - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - }) - : null, - } as never, - }); + it( + "owns anthropic 4.6 forward-compat resolution", + () => { + const provider = requireProviderContractProvider("anthropic"); + const model = provider.resolveDynamicModel?.({ + provider: "anthropic", + modelId: "claude-sonnet-4.6-20260219", + modelRegistry: { + find: (_provider: string, id: string) => + id === "claude-sonnet-4.5-20260219" + ? createModel({ + id: id, + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + }) + : null, + } as never, + }); - expect(model).toMatchObject({ - id: "claude-sonnet-4.6-20260219", - provider: "anthropic", - api: "anthropic-messages", - baseUrl: "https://api.anthropic.com", - }); - }); + expect(model).toMatchObject({ + id: "claude-sonnet-4.6-20260219", + provider: "anthropic", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com", + }); + }, + CONTRACT_SETUP_TIMEOUT_MS, + ); it("owns usage auth resolution", async () => { const provider = requireProviderContractProvider("anthropic"); @@ -519,21 +526,40 @@ describe("provider runtime contract", () => { }); describe("openai-codex", () => { - it("owns refresh fallback for accountId extraction failures", async () => { - const provider = requireProviderContractProvider("openai-codex"); - const credential = { - type: "oauth" as const, - provider: "openai-codex", - access: "cached-access-token", - refresh: "refresh-token", - expires: Date.now() - 60_000, - }; + it( + "owns refresh fallback for accountId extraction failures", + async () => { + const provider = requireProviderContractProvider("openai-codex"); + const credential = { + type: "oauth" as const, + provider: "openai-codex", + access: "cached-access-token", + refresh: "refresh-token", + expires: Date.now() - 60_000, + }; - getOAuthApiKeyMock.mockReset(); - getOAuthApiKeyMock.mockRejectedValueOnce(new Error("Failed to extract accountId from token")); + const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString( + "base64url", + ); + const payload = Buffer.from(JSON.stringify({})).toString("base64url"); + const accessTokenWithoutAccountId = `${header}.${payload}.sig`; + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn(async () => + makeResponse(200, { + access_token: accessTokenWithoutAccountId, + refresh_token: "refreshed-refresh-token", + expires_in: 3600, + }), + ) as typeof fetch; - await expect(provider.refreshOAuth?.(credential)).resolves.toEqual(credential); - }); + try { + await expect(provider.refreshOAuth?.(credential)).resolves.toEqual(credential); + } finally { + globalThis.fetch = originalFetch; + } + }, + CONTRACT_SETUP_TIMEOUT_MS, + ); it("owns forward-compat codex models", () => { const provider = requireProviderContractProvider("openai-codex");