From 2fc429dfbf0e13fa97a7a068ef65867c2a4d5e82 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Apr 2026 20:56:38 +0100 Subject: [PATCH] fix: keep codex oauth bridge extension-owned (#68284) (thanks @vincentkoc) --- CHANGELOG.md | 1 + .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- .../codex/src/app-server/auth-bridge.test.ts | 8 +- .../codex/src/app-server/auth-bridge.ts | 71 ++++++++++++++--- .../codex/src/app-server/run-attempt.test.ts | 2 +- .../openai/openai-codex-cli-auth.test.ts | 7 ++ extensions/openai/openai-codex-cli-auth.ts | 6 +- .../openai/openai-codex-cli-bridge.test.ts | 5 ++ extensions/openai/openai-codex-cli-bridge.ts | 70 ++++++++++++++--- .../openai/openai-codex-provider.test.ts | 37 ++++++++- extensions/openai/openai-codex-provider.ts | 58 +++++++++----- package.json | 4 - scripts/lib/plugin-sdk-entrypoints.json | 1 - ...th-profiles.ensureauthprofilestore.test.ts | 64 ++++++++------- .../auth-profiles/oauth-manager.test.ts | 35 +++++++-- src/agents/auth-profiles/oauth-manager.ts | 2 +- src/agents/auth-profiles/oauth-shared.ts | 9 ++- .../oauth.mirror-refresh.test.ts | 14 ++-- src/agents/auth-profiles/types.ts | 1 + src/agents/cli-credentials.test.ts | 10 ++- src/agents/cli-credentials.ts | 7 ++ src/agents/codex-auth-bridge.ts | 77 ------------------- ...gateway-codex-harness.live-helpers.test.ts | 10 +++ .../gateway-codex-harness.live-helpers.ts | 27 ++++--- src/plugin-sdk/codex-auth-bridge-runtime.ts | 8 -- src/plugin-sdk/core.ts | 18 ++++- src/plugin-sdk/provider-auth.ts | 5 +- 27 files changed, 363 insertions(+), 198 deletions(-) delete mode 100644 src/agents/codex-auth-bridge.ts delete mode 100644 src/plugin-sdk/codex-auth-bridge-runtime.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fcd2ad42d0..6fca4c1e691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai - Exec approvals/display: escape raw control characters (including newline and carriage return) in the shared and macOS approval-prompt command sanitizers, so trailing command payloads no longer render on hidden extra lines in the approval UI. (#68198) - Telegram/streaming: fence same-session stale preview and finalization work after aborts so Telegram no longer replays an older reply or flushes a hidden short preview after the abort confirmation lands. (#68100) Thanks @rubencu. - OpenAI Codex/OAuth + Pi: keep imported Codex CLI OAuth bootstrap, Pi auth export, and runtime overlay handling aligned so Codex sessions survive refresh and health checks without leaking transient CLI state into saved auth files. Thanks @vincentkoc. +- OpenAI Codex/OAuth: keep Codex-specific auth bridging inside the owning plugins, preserve canonical imported CLI profiles, and allow legacy identity-less main-store OAuth sessions to upgrade during refresh mirroring. (#68284) Thanks @vincentkoc. - Config/redact: add `browser.cdpUrl` and `browser.profiles.*.cdpUrl` to sensitive URL config paths so embedded credentials (query tokens and HTTP Basic auth) are properly redacted in `config.get` API responses and availability error messages. (#67679) Thanks @Ziy1-Tan. - Agents/TTS: report failed speech synthesis as a real tool error so unconfigured providers no longer feed successful TTS failure output back into agent loops. (#67980) Thanks @lawrence3699. - Gateway/wake: allow unknown properties on wake payloads so external senders like Paperclip can attach opaque metadata without failing schema validation. (#68355) Thanks @kagura-agent. diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index b68604f8016..9d489cde45e 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -3447b4257e1eeaf3388b2c148e0086c534ba43147004100e9baa7cb662835c79 plugin-sdk-api-baseline.json -78590addd53ec7db1ef5a56eecc93c39303344ccfad8349a14e8f97480fe64b2 plugin-sdk-api-baseline.jsonl +f2002dd0661ca25eee382e1ab14c4655cfa3f4d616eaf4ac98db307df8453687 plugin-sdk-api-baseline.json +34ad0f68b93b49eed4bce4f5e56511e3c555917c0b8743d399534d9ec3e8a8e2 plugin-sdk-api-baseline.jsonl diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index f759029b275..ff9680b3b18 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -41,10 +41,12 @@ describe("bridgeCodexAppServerStartOptions", () => { refresh: "refresh-token", expires: Date.now() + 60_000, accountId: "acct-123", + idToken: "id-token", }, }, }, agentDir, + { filterExternalAuthProfiles: false }, ); const result = await bridgeCodexAppServerStartOptions({ @@ -73,6 +75,7 @@ describe("bridgeCodexAppServerStartOptions", () => { expect(authFile).toEqual({ auth_mode: "chatgpt", tokens: { + id_token: "id-token", access_token: "access-token", refresh_token: "refresh-token", account_id: "acct-123", @@ -93,7 +96,9 @@ describe("bridgeCodexAppServerStartOptions", () => { args: ["app-server"], headers: { authorization: "Bearer dev-token" }, }; - saveAuthProfileStore({ version: 1, profiles: {} }, agentDir); + saveAuthProfileStore({ version: 1, profiles: {} }, agentDir, { + filterExternalAuthProfiles: false, + }); await expect( bridgeCodexAppServerStartOptions({ @@ -121,6 +126,7 @@ describe("bridgeCodexAppServerStartOptions", () => { }, }, agentDir, + { filterExternalAuthProfiles: false }, ); const codexHome = resolveHashedCodexHome(agentDir, "openai-codex:default"); diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index 3c68f7ce5e4..e80d1ce5c89 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -1,7 +1,54 @@ -import { prepareCodexAuthBridgeFromProfile } from "openclaw/plugin-sdk/codex-auth-bridge-runtime"; +import crypto from "node:crypto"; +import path from "node:path"; +import { + ensureAuthProfileStoreForLocalUpdate, + type OAuthCredential, +} from "openclaw/plugin-sdk/provider-auth"; +import { writePrivateSecretFileAtomic } from "openclaw/plugin-sdk/secret-file-runtime"; import type { CodexAppServerStartOptions } from "./config.js"; const DEFAULT_CODEX_AUTH_PROFILE_ID = "openai-codex:default"; +const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; +const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const; + +function isCodexBridgeableOAuthCredential(value: unknown): value is OAuthCredential { + return Boolean( + value && + typeof value === "object" && + value !== null && + "type" in value && + "provider" in value && + "access" in value && + "refresh" in value && + value.type === "oauth" && + value.provider === OPENAI_CODEX_PROVIDER_ID && + typeof value.access === "string" && + value.access.trim().length > 0 && + typeof value.refresh === "string" && + value.refresh.trim().length > 0, + ); +} + +function resolveCodexBridgeHome(agentDir: string, profileId: string): string { + const digest = crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16); + return path.join(agentDir, "harness-auth", "codex", digest); +} + +function buildCodexAuthFile(credential: OAuthCredential): string { + return `${JSON.stringify( + { + auth_mode: "chatgpt", + tokens: { + ...(credential.idToken ? { id_token: credential.idToken } : {}), + access_token: credential.access, + refresh_token: credential.refresh, + ...(credential.accountId ? { account_id: credential.accountId } : {}), + }, + }, + null, + 2, + )}\n`; +} export async function bridgeCodexAppServerStartOptions(params: { startOptions: CodexAppServerStartOptions; @@ -9,21 +56,27 @@ export async function bridgeCodexAppServerStartOptions(params: { authProfileId?: string; }): Promise { const profileId = params.authProfileId?.trim() || DEFAULT_CODEX_AUTH_PROFILE_ID; - const bridge = await prepareCodexAuthBridgeFromProfile({ - agentDir: params.agentDir, - authProfileId: profileId, - bridgeRoot: "harness-auth", - }); - if (!bridge) { + const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir); + const credential = store.profiles[profileId]; + if (!isCodexBridgeableOAuthCredential(credential)) { return params.startOptions; } + const codexHome = resolveCodexBridgeHome(params.agentDir, profileId); + await writePrivateSecretFileAtomic({ + rootDir: params.agentDir, + filePath: path.join(codexHome, "auth.json"), + content: buildCodexAuthFile(credential), + }); + return { ...params.startOptions, env: { ...params.startOptions.env, - CODEX_HOME: bridge.codexHome, + CODEX_HOME: codexHome, }, - clearEnv: Array.from(new Set([...(params.startOptions.clearEnv ?? []), ...bridge.clearEnv])), + clearEnv: Array.from( + new Set([...(params.startOptions.clearEnv ?? []), ...CODEX_AUTH_ENV_CLEAR_KEYS]), + ), }; } diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 4c505bbda40..77adfcd6803 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -532,7 +532,7 @@ describe("runCodexAppServerAttempt", () => { const binding = await startOrResumeThread({ client: { - request: async (method) => { + request: async (method: string) => { if (method === "thread/resume") { return { thread: { id: "thread-existing" }, modelProvider: "openai" }; } diff --git a/extensions/openai/openai-codex-cli-auth.test.ts b/extensions/openai/openai-codex-cli-auth.test.ts index 06659068366..7e2a5dd58be 100644 --- a/extensions/openai/openai-codex-cli-auth.test.ts +++ b/extensions/openai/openai-codex-cli-auth.test.ts @@ -42,6 +42,7 @@ describe("readOpenAICodexCliOAuthProfile", () => { JSON.stringify({ auth_mode: "chatgpt", tokens: { + id_token: "id-token", access_token: accessToken, refresh_token: "refresh-token", account_id: "acct_123", @@ -61,6 +62,7 @@ describe("readOpenAICodexCliOAuthProfile", () => { access: accessToken, refresh: "refresh-token", accountId: "acct_123", + idToken: "id-token", email: "codex@example.com", }, }); @@ -172,6 +174,7 @@ describe("readOpenAICodexCliOAuthProfile", () => { JSON.stringify({ auth_mode: "chatgpt", tokens: { + id_token: "id-token", access_token: accessToken, refresh_token: "refresh-token", account_id: "acct_123", @@ -201,6 +204,7 @@ describe("readOpenAICodexCliOAuthProfile", () => { access: accessToken, refresh: "refresh-token", accountId: "acct_123", + idToken: "id-token", email: "codex@example.com", }, }); @@ -217,6 +221,7 @@ describe("readOpenAICodexCliOAuthProfile", () => { JSON.stringify({ auth_mode: "chatgpt", tokens: { + id_token: "id-token", access_token: accessToken, refresh_token: "refresh-token", account_id: "acct_123", @@ -254,6 +259,7 @@ describe("readOpenAICodexCliOAuthProfile", () => { JSON.stringify({ auth_mode: "chatgpt", tokens: { + id_token: "id-token", access_token: accessToken, refresh_token: "refresh-token", account_id: "acct_123", @@ -281,6 +287,7 @@ describe("readOpenAICodexCliOAuthProfile", () => { access: accessToken, refresh: "refresh-token", accountId: "acct_123", + idToken: "id-token", email: "codex@example.com", }, }); diff --git a/extensions/openai/openai-codex-cli-auth.ts b/extensions/openai/openai-codex-cli-auth.ts index f847ac18a40..e4980f4f262 100644 --- a/extensions/openai/openai-codex-cli-auth.ts +++ b/extensions/openai/openai-codex-cli-auth.ts @@ -22,6 +22,7 @@ export const OPENAI_CODEX_DEFAULT_PROFILE_ID = `${PROVIDER_ID}:default`; type CodexCliAuthFile = { auth_mode?: unknown; tokens?: { + id_token?: unknown; access_token?: unknown; refresh_token?: unknown; account_id?: unknown; @@ -76,7 +77,8 @@ function oauthCredentialMatches(a: OAuthCredential, b: OAuthCredential): boolean a.displayName === b.displayName && a.enterpriseUrl === b.enterpriseUrl && a.projectId === b.projectId && - a.accountId === b.accountId + a.accountId === b.accountId && + a.idToken === b.idToken ); } @@ -131,6 +133,7 @@ export function readOpenAICodexCliOAuthProfile(params: { } const accountId = trimNonEmptyString(authFile.tokens?.account_id); + const idToken = trimNonEmptyString(authFile.tokens?.id_token); const identity = resolveCodexAuthIdentity({ accessToken: access }); const credential: OAuthCredential = { type: "oauth", @@ -139,6 +142,7 @@ export function readOpenAICodexCliOAuthProfile(params: { refresh, expires: resolveCodexAccessTokenExpiry(access) ?? 0, ...(accountId ? { accountId } : {}), + ...(idToken ? { idToken } : {}), ...(identity.email ? { email: identity.email } : {}), ...(identity.profileName ? { displayName: identity.profileName } : {}), }; diff --git a/extensions/openai/openai-codex-cli-bridge.test.ts b/extensions/openai/openai-codex-cli-bridge.test.ts index 80f21b630b2..ebff520c93d 100644 --- a/extensions/openai/openai-codex-cli-bridge.test.ts +++ b/extensions/openai/openai-codex-cli-bridge.test.ts @@ -36,10 +36,12 @@ describe("prepareOpenAICodexCliExecution", () => { refresh: "refresh-token", expires: Date.now() + 60_000, accountId: "acct-123", + idToken: "id-token", }, }, }, agentDir, + { filterExternalAuthProfiles: false }, ); const result = await prepareOpenAICodexCliExecution({ @@ -64,6 +66,7 @@ describe("prepareOpenAICodexCliExecution", () => { expect(authFile).toEqual({ auth_mode: "chatgpt", tokens: { + id_token: "id-token", access_token: "access-token", refresh_token: "refresh-token", account_id: "acct-123", @@ -90,6 +93,7 @@ describe("prepareOpenAICodexCliExecution", () => { }, }, agentDir, + { filterExternalAuthProfiles: false }, ); await expect( @@ -124,6 +128,7 @@ describe("prepareOpenAICodexCliExecution", () => { }, }, agentDir, + { filterExternalAuthProfiles: false }, ); await expect( diff --git a/extensions/openai/openai-codex-cli-bridge.ts b/extensions/openai/openai-codex-cli-bridge.ts index 25d22cb5a19..6774bac68d7 100644 --- a/extensions/openai/openai-codex-cli-bridge.ts +++ b/extensions/openai/openai-codex-cli-bridge.ts @@ -1,8 +1,56 @@ +import crypto from "node:crypto"; +import path from "node:path"; import type { CliBackendPreparedExecution, CliBackendPrepareExecutionContext, } from "openclaw/plugin-sdk/cli-backend"; -import { prepareCodexAuthBridgeFromProfile } from "openclaw/plugin-sdk/codex-auth-bridge-runtime"; +import { + ensureAuthProfileStoreForLocalUpdate, + type OAuthCredential, +} from "openclaw/plugin-sdk/provider-auth"; +import { writePrivateSecretFileAtomic } from "openclaw/plugin-sdk/secret-file-runtime"; + +const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; +const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const; + +function isCodexBridgeableOAuthCredential(value: unknown): value is OAuthCredential { + return Boolean( + value && + typeof value === "object" && + value !== null && + "type" in value && + "provider" in value && + "access" in value && + "refresh" in value && + value.type === "oauth" && + value.provider === OPENAI_CODEX_PROVIDER_ID && + typeof value.access === "string" && + value.access.trim().length > 0 && + typeof value.refresh === "string" && + value.refresh.trim().length > 0, + ); +} + +function resolveCodexBridgeHome(agentDir: string, profileId: string): string { + const digest = crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16); + return path.join(agentDir, "cli-auth", "codex", digest); +} + +function buildCodexAuthFile(credential: OAuthCredential): string { + return `${JSON.stringify( + { + auth_mode: "chatgpt", + tokens: { + ...(credential.idToken ? { id_token: credential.idToken } : {}), + access_token: credential.access, + refresh_token: credential.refresh, + ...(credential.accountId ? { account_id: credential.accountId } : {}), + }, + }, + null, + 2, + )}\n`; +} export async function prepareOpenAICodexCliExecution( ctx: CliBackendPrepareExecutionContext, @@ -11,19 +59,23 @@ export async function prepareOpenAICodexCliExecution( return null; } - const bridge = await prepareCodexAuthBridgeFromProfile({ - agentDir: ctx.agentDir, - authProfileId: ctx.authProfileId, - bridgeRoot: "cli-auth", - }); - if (!bridge) { + const store = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir); + const credential = store.profiles[ctx.authProfileId]; + if (!isCodexBridgeableOAuthCredential(credential)) { return null; } + const codexHome = resolveCodexBridgeHome(ctx.agentDir, ctx.authProfileId); + await writePrivateSecretFileAtomic({ + rootDir: ctx.agentDir, + filePath: path.join(codexHome, "auth.json"), + content: buildCodexAuthFile(credential), + }); + return { env: { - CODEX_HOME: bridge.codexHome, + CODEX_HOME: codexHome, }, - clearEnv: bridge.clearEnv, + clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS], }; } diff --git a/extensions/openai/openai-codex-provider.test.ts b/extensions/openai/openai-codex-provider.test.ts index 680157aa120..6602ae4960e 100644 --- a/extensions/openai/openai-codex-provider.test.ts +++ b/extensions/openai/openai-codex-provider.test.ts @@ -118,6 +118,41 @@ describe("openai codex provider", () => { }); }); + it("exposes Codex CLI auth as a runtime-only external profile", () => { + const provider = buildOpenAICodexProviderPlugin(); + const credential = { + type: "oauth" as const, + provider: "openai-codex", + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + accountId: "acct-123", + }; + readOpenAICodexCliOAuthProfileMock.mockReturnValueOnce({ + profileId: "openai-codex:default", + credential, + }); + + expect( + provider.resolveExternalAuthProfiles?.({ + env: { CODEX_HOME: "/sandboxed/codex-home" } as NodeJS.ProcessEnv, + store: { version: 1, profiles: {} }, + }), + ).toEqual([ + { + profileId: "openai-codex:default", + credential, + persistence: "runtime-only", + }, + ]); + expect(readOpenAICodexCliOAuthProfileMock).toHaveBeenCalledWith( + expect.objectContaining({ + env: expect.objectContaining({ CODEX_HOME: "/sandboxed/codex-home" }), + store: { version: 1, profiles: {} }, + }), + ); + }); + it("uses the provider auth context env when importing Codex CLI auth", async () => { const provider = buildOpenAICodexProviderPlugin(); const importMethod = provider.auth?.find((method) => method.id === "import-codex-cli"); @@ -156,7 +191,7 @@ describe("openai codex provider", () => { ).resolves.toMatchObject({ profiles: [ { - profileId: "default:codex@example.com", + profileId: "openai-codex:default", credential: expect.objectContaining({ provider: "openai-codex", access: "access-token", diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index 1a311f46c88..b1ef09c5c4f 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -5,9 +5,10 @@ import type { ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; import { - ensureAuthProfileStore, + ensureAuthProfileStoreForLocalUpdate, listProfilesForProvider, type OAuthCredential, + type ProviderAuthResult, } from "openclaw/plugin-sdk/provider-auth"; import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth"; import { loginOpenAICodexOAuth } from "openclaw/plugin-sdk/provider-auth-login"; @@ -285,9 +286,7 @@ async function runOpenAICodexOAuth(ctx: ProviderAuthContext) { async function runImportOpenAICodexCliAuth(ctx: ProviderAuthContext) { const profile = readOpenAICodexCliOAuthProfile({ env: ctx.env ?? process.env, - store: ensureAuthProfileStore(ctx.agentDir, { - allowKeychainPrompt: false, - }), + store: ensureAuthProfileStoreForLocalUpdate(ctx.agentDir), }); if (!profile) { throw new Error( @@ -295,20 +294,38 @@ async function runImportOpenAICodexCliAuth(ctx: ProviderAuthContext) { ); } - return buildOauthProviderAuthResult({ - providerId: PROVIDER_ID, + return { + profiles: [{ profileId: profile.profileId, credential: profile.credential }], + configPatch: { + agents: { + defaults: { + models: { + [OPENAI_CODEX_DEFAULT_MODEL]: {}, + }, + }, + }, + }, defaultModel: OPENAI_CODEX_DEFAULT_MODEL, - access: profile.credential.access, - refresh: profile.credential.refresh, - expires: profile.credential.expires, - email: profile.credential.email, - displayName: profile.credential.displayName, - profilePrefix: "default", - credentialExtra: profile.credential.accountId - ? { accountId: profile.credential.accountId } - : {}, notes: ["Imported existing Codex CLI login into OpenClaw canonical auth."], + } satisfies ProviderAuthResult; +} + +function ensureOpenAICodexCatalogAuthStore(ctx: { agentDir?: string; env?: NodeJS.ProcessEnv }) { + const store = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir); + const profile = readOpenAICodexCliOAuthProfile({ + env: ctx.env ?? process.env, + store, }); + if (!profile) { + return store; + } + return { + ...store, + profiles: { + ...store.profiles, + [profile.profileId]: profile.credential, + }, + }; } function buildOpenAICodexAuthDoctorHint(ctx: { profileId?: string }) { @@ -350,9 +367,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { catalog: { order: "profile", run: async (ctx) => { - const authStore = ensureAuthProfileStore(ctx.agentDir, { - allowKeychainPrompt: false, - }); + const authStore = ensureOpenAICodexCatalogAuthStore(ctx); if (listProfilesForProvider(authStore, PROVIDER_ID).length === 0) { return null; } @@ -395,6 +410,13 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { fetchUsageSnapshot: async (ctx) => await fetchCodexUsage(ctx.token, ctx.accountId, ctx.timeoutMs, ctx.fetchFn), refreshOAuth: async (cred) => await refreshOpenAICodexOAuthCredential(cred), + resolveExternalAuthProfiles: (ctx) => { + const profile = readOpenAICodexCliOAuthProfile({ + env: ctx.env, + store: ctx.store, + }); + return profile ? [{ ...profile, persistence: "runtime-only" }] : []; + }, augmentModelCatalog: (ctx) => { const gpt54Template = findCatalogTemplate({ entries: ctx.entries, diff --git a/package.json b/package.json index 08bc7a3eff1..14de46be320 100644 --- a/package.json +++ b/package.json @@ -305,10 +305,6 @@ "types": "./dist/plugin-sdk/agent-runtime.d.ts", "default": "./dist/plugin-sdk/agent-runtime.js" }, - "./plugin-sdk/codex-auth-bridge-runtime": { - "types": "./dist/plugin-sdk/codex-auth-bridge-runtime.d.ts", - "default": "./dist/plugin-sdk/codex-auth-bridge-runtime.js" - }, "./plugin-sdk/simple-completion-runtime": { "types": "./dist/plugin-sdk/simple-completion-runtime.d.ts", "default": "./dist/plugin-sdk/simple-completion-runtime.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 27b73cc8133..fc22e141391 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -62,7 +62,6 @@ "text-runtime", "text-chunking", "agent-runtime", - "codex-auth-bridge-runtime", "simple-completion-runtime", "speech-core", "plugin-runtime", diff --git a/src/agents/auth-profiles.ensureauthprofilestore.test.ts b/src/agents/auth-profiles.ensureauthprofilestore.test.ts index 5513a9e6199..ab064fc554c 100644 --- a/src/agents/auth-profiles.ensureauthprofilestore.test.ts +++ b/src/agents/auth-profiles.ensureauthprofilestore.test.ts @@ -1,7 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { ProviderExternalAuthProfile } from "../plugins/provider-external-auth.types.js"; import { clearRuntimeAuthProfileStoreSnapshots, ensureAuthProfileStore, @@ -10,8 +11,12 @@ import { import { AUTH_STORE_VERSION, log } from "./auth-profiles/constants.js"; import type { AuthProfileCredential } from "./auth-profiles/types.js"; +const resolveExternalAuthProfilesWithPluginsMock = vi.hoisted(() => + vi.fn<() => ProviderExternalAuthProfile[]>(() => []), +); + vi.mock("../plugins/provider-runtime.js", () => ({ - resolveExternalAuthProfilesWithPlugins: () => [], + resolveExternalAuthProfilesWithPlugins: resolveExternalAuthProfilesWithPluginsMock, })); vi.mock("./cli-credentials.js", () => ({ @@ -51,6 +56,11 @@ vi.mock("./cli-credentials.js", () => ({ })); describe("ensureAuthProfileStore", () => { + afterEach(() => { + resolveExternalAuthProfilesWithPluginsMock.mockReset(); + resolveExternalAuthProfilesWithPluginsMock.mockReturnValue([]); + }); + function withTempAgentDir(prefix: string, run: (agentDir: string) => T): T { const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); try { @@ -385,55 +395,43 @@ describe("ensureAuthProfileStore", () => { } }); - it("exposes Codex CLI auth without persisting copied tokens into auth-profiles.json", () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-external-sync-")); - const previousCodexHome = process.env.CODEX_HOME; + it("exposes provider-managed runtime auth without persisting copied tokens", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-external-auth-")); const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; try { const agentDir = path.join(root, "agent"); - const codexHome = path.join(root, "codex-home"); fs.mkdirSync(agentDir, { recursive: true }); - fs.mkdirSync(codexHome, { recursive: true }); - fs.writeFileSync( - path.join(codexHome, "auth.json"), - `${JSON.stringify( - { - auth_mode: "chatgpt", - tokens: { - access_token: "codex-access-token", - refresh_token: "codex-refresh-token", - account_id: "acct_123", - }, - last_refresh: "2026-03-01T00:00:00.000Z", + resolveExternalAuthProfilesWithPluginsMock.mockReturnValueOnce([ + { + profileId: "demo-provider:external", + credential: { + type: "oauth", + provider: "demo-provider", + access: "external-access-token", + refresh: "external-refresh-token", + expires: Date.now() + 60_000, + accountId: "acct_123", }, - null, - 2, - )}\n`, - "utf8", - ); + persistence: "runtime-only", + }, + ]); - process.env.CODEX_HOME = codexHome; process.env.OPENCLAW_AGENT_DIR = agentDir; process.env.PI_CODING_AGENT_DIR = agentDir; clearRuntimeAuthProfileStoreSnapshots(); const store = ensureAuthProfileStore(agentDir); - expect(store.profiles["openai-codex:default"]).toMatchObject({ + expect(store.profiles["demo-provider:external"]).toMatchObject({ type: "oauth", - provider: "openai-codex", - access: "codex-access-token", - refresh: "codex-refresh-token", + provider: "demo-provider", + access: "external-access-token", + refresh: "external-refresh-token", }); expect(fs.existsSync(path.join(agentDir, "auth-profiles.json"))).toBe(false); } finally { clearRuntimeAuthProfileStoreSnapshots(); - if (previousCodexHome === undefined) { - delete process.env.CODEX_HOME; - } else { - process.env.CODEX_HOME = previousCodexHome; - } if (previousAgentDir === undefined) { delete process.env.OPENCLAW_AGENT_DIR; } else { diff --git a/src/agents/auth-profiles/oauth-manager.test.ts b/src/agents/auth-profiles/oauth-manager.test.ts index 02797859f6d..259bf9b916d 100644 --- a/src/agents/auth-profiles/oauth-manager.test.ts +++ b/src/agents/auth-profiles/oauth-manager.test.ts @@ -1,7 +1,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { captureEnv } from "../../test-utils/env.js"; +import { __testing as externalAuthTesting } from "./external-auth.js"; import { createOAuthManager, isSafeToAdoptBootstrapOAuthIdentity, @@ -9,7 +11,11 @@ import { isSafeToOverwriteStoredOAuthIdentity, OAuthManagerRefreshError, } from "./oauth-manager.js"; -import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js"; +import { + clearRuntimeAuthProfileStoreSnapshots, + ensureAuthProfileStore, + saveAuthProfileStore, +} from "./store.js"; import type { AuthProfileStore, OAuthCredential } from "./types.js"; function createCredential(overrides: Partial = {}): OAuthCredential { @@ -24,8 +30,17 @@ function createCredential(overrides: Partial = {}): OAuthCreden } const tempDirs: string[] = []; +const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR", "PI_CODING_AGENT_DIR"]); + +beforeEach(() => { + externalAuthTesting.setResolveExternalAuthProfilesForTest(() => []); + clearRuntimeAuthProfileStoreSnapshots(); +}); afterEach(async () => { + envSnapshot.restore(); + externalAuthTesting.resetResolveExternalAuthProfilesForTest(); + clearRuntimeAuthProfileStoreSnapshots(); await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); }); @@ -75,7 +90,7 @@ describe("isSafeToOverwriteStoredOAuthIdentity", () => { }); describe("isSafeToAdoptMainStoreOAuthIdentity", () => { - it("requires positive identity binding before adopting from the main store", () => { + it("allows identity-less credentials to adopt from the main store", () => { expect( isSafeToAdoptMainStoreOAuthIdentity( createCredential({ @@ -88,7 +103,7 @@ describe("isSafeToAdoptMainStoreOAuthIdentity", () => { accountId: "acct-main", }), ), - ).toBe(false); + ).toBe(true); }); it("accepts matching account identities", () => { @@ -131,8 +146,15 @@ describe("OAuthManagerRefreshError", () => { describe("createOAuthManager", () => { it("refreshes with the adopted external oauth credential", async () => { - const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-")); - tempDirs.push(agentDir); + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-")); + tempDirs.push(tempRoot); + process.env.OPENCLAW_STATE_DIR = tempRoot; + const mainAgentDir = path.join(tempRoot, "agents", "main", "agent"); + const agentDir = path.join(tempRoot, "agents", "sub", "agent"); + process.env.OPENCLAW_AGENT_DIR = mainAgentDir; + process.env.PI_CODING_AGENT_DIR = mainAgentDir; + await fs.mkdir(agentDir, { recursive: true }); + await fs.mkdir(mainAgentDir, { recursive: true }); const profileId = "minimax-portal:default"; const localCredential = createCredential({ provider: "minimax-portal", @@ -148,6 +170,7 @@ describe("createOAuthManager", () => { }, }, agentDir, + { filterExternalAuthProfiles: false }, ); const manager = createOAuthManager({ diff --git a/src/agents/auth-profiles/oauth-manager.ts b/src/agents/auth-profiles/oauth-manager.ts index 25878517710..7b21990a92a 100644 --- a/src/agents/auth-profiles/oauth-manager.ts +++ b/src/agents/auth-profiles/oauth-manager.ts @@ -310,7 +310,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) { if (existing && existing.provider !== params.refreshed.provider) { return false; } - if (existing && !isSafeToOverwriteStoredOAuthIdentity(existing, params.refreshed)) { + if (existing && !isSafeToAdoptMainStoreOAuthIdentity(existing, params.refreshed)) { log.warn("refused to mirror OAuth credential: identity mismatch or regression", { profileId: params.profileId, }); diff --git a/src/agents/auth-profiles/oauth-shared.ts b/src/agents/auth-profiles/oauth-shared.ts index c03d77214e5..32c5d797369 100644 --- a/src/agents/auth-profiles/oauth-shared.ts +++ b/src/agents/auth-profiles/oauth-shared.ts @@ -22,7 +22,8 @@ export function areOAuthCredentialsEquivalent( a.email === b.email && a.enterpriseUrl === b.enterpriseUrl && a.projectId === b.projectId && - a.accountId === b.accountId + a.accountId === b.accountId && + a.idToken === b.idToken ); } @@ -141,6 +142,12 @@ export function isSafeToAdoptMainStoreOAuthIdentity( if (existing.provider !== incoming.provider) { return false; } + if (areOAuthCredentialsEquivalent(existing, incoming)) { + return true; + } + if (!hasOAuthIdentity(existing)) { + return true; + } return hasMatchingOAuthIdentity(existing, incoming); } diff --git a/src/agents/auth-profiles/oauth.mirror-refresh.test.ts b/src/agents/auth-profiles/oauth.mirror-refresh.test.ts index 911c6e90007..74c29cb0b49 100644 --- a/src/agents/auth-profiles/oauth.mirror-refresh.test.ts +++ b/src/agents/auth-profiles/oauth.mirror-refresh.test.ts @@ -652,10 +652,9 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () => }); }); - it("refuses to mirror into a main store that lacks identity binding", async () => { - // Authoritative main-store overwrites now require a positive identity - // match. A pre-capture main credential must not be replaced by a - // sub-agent refresh from an arbitrary account. + it("mirrors identity-bearing refreshes into a pre-capture main store", async () => { + // Pre-capture main credentials may lack account identity. Allow the + // refreshed sub-agent credential to upgrade the main store with identity. const profileId = "openai-codex:default"; const provider = "openai-codex"; const freshExpiry = Date.now() + 60 * 60 * 1000; @@ -703,14 +702,13 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () => }); expect(result?.apiKey).toBe("sub-refreshed-access"); - // Main must remain unchanged because there was no positive identity - // binding to authorize the overwrite. const mainRaw = JSON.parse( await fs.readFile(path.join(mainAgentDir, "auth-profiles.json"), "utf8"), ) as AuthProfileStore; expect(mainRaw.profiles[profileId]).toMatchObject({ - access: "main-pre-capture-access", - refresh: "main-pre-capture-refresh", + access: "sub-refreshed-access", + refresh: "sub-refreshed-refresh", + accountId: "acct-sub", }); }); diff --git a/src/agents/auth-profiles/types.ts b/src/agents/auth-profiles/types.ts index 0ee11504310..0459b640a16 100644 --- a/src/agents/auth-profiles/types.ts +++ b/src/agents/auth-profiles/types.ts @@ -12,6 +12,7 @@ export type OAuthCredentials = { enterpriseUrl?: string; projectId?: string; accountId?: string; + idToken?: string; }; export type ApiKeyCredential = { diff --git a/src/agents/cli-credentials.test.ts b/src/agents/cli-credentials.test.ts index 262454dbaf9..c2291e04be7 100644 --- a/src/agents/cli-credentials.test.ts +++ b/src/agents/cli-credentials.test.ts @@ -255,6 +255,7 @@ describe("cli credentials", () => { expect(cmd).toContain(accountHash); return JSON.stringify({ tokens: { + id_token: "keychain-id-token", access_token: createJwtWithExp(expSeconds), refresh_token: "keychain-refresh", }, @@ -269,6 +270,7 @@ describe("cli credentials", () => { refresh: "keychain-refresh", provider: "openai-codex", expires: expSeconds * 1000, + idToken: "keychain-id-token", }); }); @@ -286,6 +288,7 @@ describe("cli credentials", () => { authPath, JSON.stringify({ tokens: { + id_token: "file-id-token", access_token: createJwtWithExp(expSeconds), refresh_token: "file-refresh", }, @@ -300,6 +303,7 @@ describe("cli credentials", () => { refresh: "file-refresh", provider: "openai-codex", expires: expSeconds * 1000, + idToken: "file-id-token", }); }); @@ -393,6 +397,7 @@ describe("cli credentials", () => { access: "new-access", refresh: "new-refresh", expires: Date.now() + 60_000, + idToken: "new-id-token", accountId: "acct-new", }); @@ -403,7 +408,7 @@ describe("cli credentials", () => { OPENAI_API_KEY: "sk-existing", }); expect(persisted.tokens).toMatchObject({ - id_token: "id-token", + id_token: "new-id-token", access_token: "new-access", refresh_token: "new-refresh", account_id: "acct-new", @@ -439,6 +444,7 @@ describe("cli credentials", () => { access: "new-access", refresh: "new-refresh", expires: Date.now() + 60_000, + idToken: "new-id-token", accountId: "acct-new", }, { @@ -460,7 +466,7 @@ describe("cli credentials", () => { expect(payload).toBeDefined(); const parsed = JSON.parse(String(payload)) as Record; expect(parsed.tokens).toMatchObject({ - id_token: "id-token", + id_token: "new-id-token", access_token: "new-access", refresh_token: "new-refresh", account_id: "acct-new", diff --git a/src/agents/cli-credentials.ts b/src/agents/cli-credentials.ts index d2b906e2897..6fb72c1eb9d 100644 --- a/src/agents/cli-credentials.ts +++ b/src/agents/cli-credentials.ts @@ -56,6 +56,7 @@ export type CodexCliCredential = { refresh: string; expires: number; accountId?: string; + idToken?: string; }; export type MiniMaxCliCredential = { @@ -289,6 +290,7 @@ function readCodexKeychainCredentials(options?: { : Date.now() + 60 * 60 * 1000; const expires = decodeJwtExpiryMs(accessToken) ?? fallbackExpiry; const accountId = typeof tokens?.account_id === "string" ? tokens.account_id : undefined; + const idToken = typeof tokens?.id_token === "string" ? tokens.id_token : undefined; log.info("read codex credentials from keychain", { source: "keychain", @@ -302,6 +304,7 @@ function readCodexKeychainCredentials(options?: { refresh: refreshToken, expires, accountId, + idToken, }; } catch { return null; @@ -542,6 +545,9 @@ function buildUpdatedCodexAuthRecord( ...existingTokens, access_token: newCredentials.access, refresh_token: newCredentials.refresh, + ...(typeof newCredentials.idToken === "string" && newCredentials.idToken.trim().length > 0 + ? { id_token: newCredentials.idToken } + : {}), ...(typeof newCredentials.accountId === "string" && newCredentials.accountId.trim().length > 0 ? { account_id: newCredentials.accountId } : {}), @@ -698,6 +704,7 @@ export function readCodexCliCredentials(options?: { refresh: refreshToken, expires, accountId: typeof tokens.account_id === "string" ? tokens.account_id : undefined, + idToken: typeof tokens.id_token === "string" ? tokens.id_token : undefined, }; } diff --git a/src/agents/codex-auth-bridge.ts b/src/agents/codex-auth-bridge.ts deleted file mode 100644 index 82aa087a042..00000000000 --- a/src/agents/codex-auth-bridge.ts +++ /dev/null @@ -1,77 +0,0 @@ -import crypto from "node:crypto"; -import path from "node:path"; -import { writePrivateSecretFileAtomic } from "../infra/secret-file.js"; -import { loadAuthProfileStoreForSecretsRuntime } from "./auth-profiles/store.js"; -import type { OAuthCredential } from "./auth-profiles/types.js"; - -export const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; -export const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const; - -export function isCodexBridgeableOAuthCredential(value: unknown): value is OAuthCredential { - return Boolean( - value && - typeof value === "object" && - value !== null && - "type" in value && - "provider" in value && - "access" in value && - "refresh" in value && - value.type === "oauth" && - value.provider === OPENAI_CODEX_PROVIDER_ID && - typeof value.access === "string" && - value.access.trim().length > 0 && - typeof value.refresh === "string" && - value.refresh.trim().length > 0, - ); -} - -export function resolveCodexBridgeHome( - agentDir: string, - profileId: string, - bridgeRoot: "cli-auth" | "harness-auth", -): string { - const digest = crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16); - return path.join(agentDir, bridgeRoot, "codex", digest); -} - -export function buildCodexAuthFile(credential: OAuthCredential): string { - return `${JSON.stringify( - { - auth_mode: "chatgpt", - tokens: { - access_token: credential.access, - refresh_token: credential.refresh, - ...(credential.accountId ? { account_id: credential.accountId } : {}), - }, - }, - null, - 2, - )}\n`; -} - -export async function prepareCodexAuthBridgeFromProfile(params: { - agentDir: string; - authProfileId: string; - bridgeRoot: "cli-auth" | "harness-auth"; -}): Promise<{ codexHome: string; clearEnv: string[] } | null> { - const store = loadAuthProfileStoreForSecretsRuntime(params.agentDir); - const credential = store.profiles[params.authProfileId]; - if (!isCodexBridgeableOAuthCredential(credential)) { - return null; - } - - const codexHome = resolveCodexBridgeHome( - params.agentDir, - params.authProfileId, - params.bridgeRoot, - ); - await writePrivateSecretFileAtomic({ - rootDir: params.agentDir, - filePath: path.join(codexHome, "auth.json"), - content: buildCodexAuthFile(credential), - }); - return { - codexHome, - clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS], - }; -} diff --git a/src/gateway/gateway-codex-harness.live-helpers.test.ts b/src/gateway/gateway-codex-harness.live-helpers.test.ts index 23bb426b514..e1904b13856 100644 --- a/src/gateway/gateway-codex-harness.live-helpers.test.ts +++ b/src/gateway/gateway-codex-harness.live-helpers.test.ts @@ -32,6 +32,16 @@ describe("gateway codex harness live helpers", () => { expect(isExpectedCodexModelsCommandText(text)).toBe(true); }); + it("accepts sandbox namespace failures with current-session model fallback", () => { + const text = [ + "I can’t enumerate `/codex models` from this sandbox because the local `codex` CLI fails to start here with a user-namespace restriction (`bwrap: No permissions to create a new namespace`).", + "", + "What I can confirm from the current session is that it’s running on `codex/gpt-5.4`.", + ].join("\n"); + + expect(isExpectedCodexModelsCommandText(text)).toBe(true); + }); + it("rejects unrelated codex command output", () => { expect(isExpectedCodexModelsCommandText("Codex is healthy.")).toBe(false); }); diff --git a/src/gateway/gateway-codex-harness.live-helpers.ts b/src/gateway/gateway-codex-harness.live-helpers.ts index 7715699930d..7ecf8465039 100644 --- a/src/gateway/gateway-codex-harness.live-helpers.ts +++ b/src/gateway/gateway-codex-harness.live-helpers.ts @@ -42,18 +42,23 @@ export const EXPECTED_CODEX_MODELS_COMMAND_TEXT = [ export function isExpectedCodexModelsCommandText(text: string): boolean { const normalized = text.toLowerCase(); + const mentionsCodexModelsCommand = + text.includes("`codex models`") || text.includes("`/codex models`"); const isSandboxFallback = - text.includes("`codex models`") && - (text.includes("did not run") || - text.includes("could not run") || - text.includes("could not be run") || - text.includes("failed in this sandbox") || - text.includes("failed with:") || - text.includes("repo-local fallback") || - text.includes("sandbox blocks") || - text.includes("interactive in this environment") || - text.includes("sandboxed session") || - text.includes("required user namespace")); + mentionsCodexModelsCommand && + (normalized.includes("did not run") || + normalized.includes("could not run") || + normalized.includes("could not be run") || + normalized.includes("failed in this sandbox") || + normalized.includes("failed with:") || + normalized.includes("fails to start") || + normalized.includes("repo-local fallback") || + normalized.includes("sandbox blocks") || + normalized.includes("interactive in this environment") || + normalized.includes("sandboxed session") || + normalized.includes("required user namespace") || + normalized.includes("user-namespace restriction") || + normalized.includes("bwrap: no permissions to create a new namespace")); const mentionsConfiguredModels = normalized.includes("configured model") || diff --git a/src/plugin-sdk/codex-auth-bridge-runtime.ts b/src/plugin-sdk/codex-auth-bridge-runtime.ts deleted file mode 100644 index fd1269fac20..00000000000 --- a/src/plugin-sdk/codex-auth-bridge-runtime.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - buildCodexAuthFile, - CODEX_AUTH_ENV_CLEAR_KEYS, - isCodexBridgeableOAuthCredential, - OPENAI_CODEX_PROVIDER_ID, - prepareCodexAuthBridgeFromProfile, - resolveCodexBridgeHome, -} from "../agents/codex-auth-bridge.js"; diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 467b193b62e..53b11cd4d33 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -24,6 +24,7 @@ import type { ReplyToMode } from "../config/types.base.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js"; import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js"; +import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js"; import type { PluginRuntime } from "../plugins/runtime/types.js"; import type { OpenClawPluginApi } from "../plugins/types.js"; @@ -227,11 +228,22 @@ export type ChannelOutboundSessionRouteParams = Parameters< NonNullable >[0]; -var cachedSdkChatChannelMeta: ReturnType | undefined; +var cachedSdkChatChannelMeta: + | { + cacheKey: string; + metaById: ReturnType; + } + | undefined; function resolveSdkChatChannelMeta(id: string) { - cachedSdkChatChannelMeta ??= buildChatChannelMetaById(); - return cachedSdkChatChannelMeta[id]; + const cacheKey = resolveBundledPluginsDir(process.env) ?? ""; + if (cachedSdkChatChannelMeta?.cacheKey !== cacheKey) { + cachedSdkChatChannelMeta = { + cacheKey, + metaById: buildChatChannelMetaById(), + }; + } + return cachedSdkChatChannelMeta.metaById[id]; } export function getChatChannelMeta(id: ChatChannelId): ChannelMeta { diff --git a/src/plugin-sdk/provider-auth.ts b/src/plugin-sdk/provider-auth.ts index 8e963e90c2f..14fc03f3e3e 100644 --- a/src/plugin-sdk/provider-auth.ts +++ b/src/plugin-sdk/provider-auth.ts @@ -12,7 +12,10 @@ export type { ProviderAuthContext } from "../plugins/types.js"; export type { AuthProfileStore, OAuthCredential } from "../agents/auth-profiles/types.js"; export { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID } from "../agents/auth-profiles/constants.js"; -export { ensureAuthProfileStore } from "../agents/auth-profiles/store.js"; +export { + ensureAuthProfileStore, + ensureAuthProfileStoreForLocalUpdate, +} from "../agents/auth-profiles/store.js"; export { listProfilesForProvider, removeProviderAuthProfilesWithLock,