From 660e4257a7eb2e5bbc3f865a100d758b2a945566 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 21 Apr 2026 00:48:13 +0100 Subject: [PATCH] refactor: share codex auth bridge --- .../codex/src/app-server/auth-bridge.ts | 71 +++------------- extensions/openai/openai-codex-cli-bridge.ts | 70 +++------------- src/plugin-sdk/provider-auth-runtime.test.ts | 4 + src/plugin-sdk/provider-auth-runtime.ts | 80 +++++++++++++++++++ 4 files changed, 102 insertions(+), 123 deletions(-) diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index e80d1ce5c89..2d710ff4a57 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -1,54 +1,7 @@ -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 { prepareCodexAuthBridge } from "openclaw/plugin-sdk/provider-auth-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; @@ -56,27 +9,21 @@ export async function bridgeCodexAppServerStartOptions(params: { authProfileId?: string; }): Promise { const profileId = params.authProfileId?.trim() || DEFAULT_CODEX_AUTH_PROFILE_ID; - const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir); - const credential = store.profiles[profileId]; - if (!isCodexBridgeableOAuthCredential(credential)) { + const bridge = await prepareCodexAuthBridge({ + agentDir: params.agentDir, + bridgeDir: "harness-auth", + profileId, + }); + if (!bridge) { 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: codexHome, + CODEX_HOME: bridge.codexHome, }, - clearEnv: Array.from( - new Set([...(params.startOptions.clearEnv ?? []), ...CODEX_AUTH_ENV_CLEAR_KEYS]), - ), + clearEnv: Array.from(new Set([...(params.startOptions.clearEnv ?? []), ...bridge.clearEnv])), }; } diff --git a/extensions/openai/openai-codex-cli-bridge.ts b/extensions/openai/openai-codex-cli-bridge.ts index 6774bac68d7..fbfa92cb6eb 100644 --- a/extensions/openai/openai-codex-cli-bridge.ts +++ b/extensions/openai/openai-codex-cli-bridge.ts @@ -1,56 +1,8 @@ -import crypto from "node:crypto"; -import path from "node:path"; import type { CliBackendPreparedExecution, CliBackendPrepareExecutionContext, } from "openclaw/plugin-sdk/cli-backend"; -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`; -} +import { prepareCodexAuthBridge } from "openclaw/plugin-sdk/provider-auth-runtime"; export async function prepareOpenAICodexCliExecution( ctx: CliBackendPrepareExecutionContext, @@ -59,23 +11,19 @@ export async function prepareOpenAICodexCliExecution( return null; } - const store = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir); - const credential = store.profiles[ctx.authProfileId]; - if (!isCodexBridgeableOAuthCredential(credential)) { + const bridge = await prepareCodexAuthBridge({ + agentDir: ctx.agentDir, + bridgeDir: "cli-auth", + profileId: ctx.authProfileId, + }); + if (!bridge) { 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: codexHome, + CODEX_HOME: bridge.codexHome, }, - clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS], + clearEnv: bridge.clearEnv, }; } diff --git a/src/plugin-sdk/provider-auth-runtime.test.ts b/src/plugin-sdk/provider-auth-runtime.test.ts index e0c89fe58ac..fc840212786 100644 --- a/src/plugin-sdk/provider-auth-runtime.test.ts +++ b/src/plugin-sdk/provider-auth-runtime.test.ts @@ -5,4 +5,8 @@ describe("plugin-sdk provider-auth-runtime", () => { it("exports the runtime-ready auth helper", () => { expect(typeof providerAuthRuntime.getRuntimeAuthForModel).toBe("function"); }); + + it("exports the Codex auth bridge helper", () => { + expect(typeof providerAuthRuntime.prepareCodexAuthBridge).toBe("function"); + }); }); diff --git a/src/plugin-sdk/provider-auth-runtime.ts b/src/plugin-sdk/provider-auth-runtime.ts index 605116d512d..f5717564207 100644 --- a/src/plugin-sdk/provider-auth-runtime.ts +++ b/src/plugin-sdk/provider-auth-runtime.ts @@ -1,8 +1,12 @@ // Public runtime auth helpers for provider plugins. +import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { ensureAuthProfileStoreForLocalUpdate } from "../agents/auth-profiles/store.js"; +import type { OAuthCredential } from "../agents/auth-profiles/types.js"; +import { writePrivateSecretFileAtomic } from "../infra/secret-file.js"; export { resolveEnvApiKey } from "../agents/model-auth-env.js"; export { @@ -18,6 +22,82 @@ export { export type { ProviderPreparedRuntimeAuth } from "../plugins/types.js"; export type { ResolvedProviderRuntimeAuth } from "../plugins/runtime/model-auth-types.js"; +export const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const; + +const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; + +export type PreparedCodexAuthBridge = { + codexHome: string; + clearEnv: string[]; +}; + +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 resolveCodexAuthBridgeHome(params: { + agentDir: string; + bridgeDir: string; + profileId: string; +}): string { + const digest = crypto.createHash("sha256").update(params.profileId).digest("hex").slice(0, 16); + return path.join(params.agentDir, params.bridgeDir, "codex", digest); +} + +export function buildCodexAuthBridgeFile(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 prepareCodexAuthBridge(params: { + agentDir: string; + bridgeDir: string; + profileId: string; +}): Promise { + const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir); + const credential = store.profiles[params.profileId]; + if (!isCodexBridgeableOAuthCredential(credential)) { + return undefined; + } + + const codexHome = resolveCodexAuthBridgeHome(params); + await writePrivateSecretFileAtomic({ + rootDir: params.agentDir, + filePath: path.join(codexHome, "auth.json"), + content: buildCodexAuthBridgeFile(credential), + }); + + return { + codexHome, + clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS], + }; +} + type ResolveApiKeyForProvider = typeof import("../agents/model-auth.js").resolveApiKeyForProvider; type GetRuntimeAuthForModel = typeof import("../plugins/runtime/runtime-model-auth.runtime.js").getRuntimeAuthForModel;