// Codex plugin module implements auth behavior. import { loadAuthProfileStoreWithoutExternalProfiles } from "openclaw/plugin-sdk/agent-runtime"; import { createMigrationItem, markMigrationItemConflict, markMigrationItemError, markMigrationItemSkipped, mergeMigrationConfigValue, resolveMigrationConfigRuntime, } from "openclaw/plugin-sdk/migration"; import type { MigrationItem, MigrationProviderContext } from "openclaw/plugin-sdk/plugin-entry"; import { applyAuthProfileConfig, buildApiKeyCredential, buildOpenAICodexCredentialExtra, buildOauthProviderAuthResult, readCodexCliCredentialsCached, resolveOpenAICodexAuthIdentity, resolveOpenAICodexImportProfileName, updateAuthProfileStoreWithLock, type AuthProfileStore, type OAuthCredential, type OpenClawConfig, type ProviderAuthResult, } from "openclaw/plugin-sdk/provider-auth"; import { isRecord, normalizeOptionalString as readString, } from "openclaw/plugin-sdk/string-coerce-runtime"; import { readJsonObject } from "./helpers.js"; import type { CodexSource } from "./source.js"; import type { resolveCodexMigrationTargets } from "./targets.js"; const OPENAI_PROVIDER_ID = "openai"; const OPENAI_CODEX_DEFAULT_MODEL = "openai/gpt-5.6-sol"; const CODEX_IMPORT_DISPLAY_NAME = "Codex import"; const CODEX_REASON_AUTH_NOT_SELECTED = "auth credential migration not selected"; const CODEX_REASON_AUTH_PROFILE_EXISTS = "auth profile exists"; const CODEX_REASON_AUTH_PROFILE_WRITE_FAILED = "failed to write auth profile"; const CODEX_REASON_AUTH_NO_LONGER_PRESENT = "auth credential no longer present"; const CODEX_REASON_MISSING_AUTH_METADATA = "missing auth metadata"; const CODEX_CONFIG_PATCH_MODE_RETURN = "return"; type CodexMigrationTargets = ReturnType; export type CodexAuthSource = Pick; type CodexAuthCredential = | { kind: "oauth"; provider: typeof OPENAI_PROVIDER_ID; profileId: string; result: ProviderAuthResult; } | { kind: "api_key"; provider: typeof OPENAI_PROVIDER_ID; profileId: string; key: string; }; type CodexAuthProfileConfig = { profileId: string; provider: string; mode: "api_key" | "oauth"; email?: string; displayName?: string; }; type CodexAuthConfigApplyResult = "configured" | "conflict" | "unavailable"; class CodexAuthConfigConflict extends Error {} async function readModelRefs(source: CodexAuthSource): Promise { const cache = await readJsonObject(source.modelsCachePath); const models = Array.isArray(cache.models) ? cache.models : []; const refs = new Set(); for (const model of models) { const slug = typeof model === "string" ? model.trim() : isRecord(model) ? (readString(model.slug) ?? readString(model.id) ?? readString(model.name)) : undefined; if (!slug) { continue; } refs.add(`${OPENAI_PROVIDER_ID}/${slug}`); } refs.add(OPENAI_CODEX_DEFAULT_MODEL); return [...refs].toSorted(); } async function buildCodexOAuthCredential( source: CodexAuthSource, ): Promise { const credential = readCodexCliCredentialsCached({ codexHome: source.codexHome, allowKeychainPrompt: false, ttlMs: 0, }); if (!credential) { return null; } const identity = resolveOpenAICodexAuthIdentity({ access: credential.access, accountId: credential.accountId, }); const modelRefs = await readModelRefs(source); const configPatch = { agents: { defaults: { models: Object.fromEntries(modelRefs.map((modelRef) => [modelRef, {}])), }, }, } satisfies Partial; const result = buildOauthProviderAuthResult({ providerId: OPENAI_PROVIDER_ID, defaultModel: OPENAI_CODEX_DEFAULT_MODEL, access: credential.access, refresh: credential.refresh, expires: credential.expires, email: identity.email, profileName: resolveOpenAICodexImportProfileName(identity, "codex-import"), displayName: CODEX_IMPORT_DISPLAY_NAME, credentialExtra: buildOpenAICodexCredentialExtra({ accountId: identity.accountId, chatgptPlanType: identity.chatgptPlanType, idToken: credential.idToken, }), configPatch, }); const profile = result.profiles[0]; return profile ? { kind: "oauth", provider: OPENAI_PROVIDER_ID, profileId: profile.profileId, result, } : null; } async function buildCodexApiKeyCredential( source: CodexAuthSource, ): Promise { const raw = await readJsonObject(source.authPath); const key = readString(raw.OPENAI_API_KEY); if (!key) { return null; } return { kind: "api_key", provider: OPENAI_PROVIDER_ID, profileId: "openai:codex-import", key, }; } async function readCodexAuthCredentials(source: CodexAuthSource): Promise { const oauth = await buildCodexOAuthCredential(source); const apiKey = await buildCodexApiKeyCredential(source); return [oauth, apiKey].filter((entry): entry is CodexAuthCredential => entry !== null); } function findMatchingOAuthProfile( store: AuthProfileStore, credential: OAuthCredential, ): string | undefined { for (const [profileId, existing] of Object.entries(store.profiles)) { if (existing.type !== "oauth" || existing.provider !== credential.provider) { continue; } if (credential.accountId && existing.accountId === credential.accountId) { return profileId; } const canMatchByEmail = !credential.accountId || !existing.accountId; if (canMatchByEmail && credential.email && existing.email === credential.email) { return profileId; } } return undefined; } function findMatchingApiKeyProfile( store: AuthProfileStore, provider: string, key: string, ): string | undefined { for (const [profileId, existing] of Object.entries(store.profiles)) { if (existing.type === "api_key" && existing.provider === provider && existing.key === key) { return profileId; } } return undefined; } function itemProfileTarget( credential: CodexAuthCredential, store: AuthProfileStore, ): { profileId: string; matchedExisting: boolean } { if (credential.kind === "oauth") { const profile = credential.result.profiles[0]; const matched = profile?.credential.type === "oauth" ? findMatchingOAuthProfile(store, profile.credential) : undefined; return { profileId: matched ?? credential.profileId, matchedExisting: Boolean(matched) }; } const matched = findMatchingApiKeyProfile(store, credential.provider, credential.key); return { profileId: matched ?? credential.profileId, matchedExisting: Boolean(matched) }; } function replaceConfigDraft(draft: OpenClawConfig, next: OpenClawConfig): void { for (const key of Object.keys(draft) as Array) { delete draft[key]; } Object.assign(draft, next); } function existingAuthProfileConfigIsCompatible( existing: NonNullable["profiles"]>[string], profile: CodexAuthProfileConfig, ): boolean { if (existing.provider !== profile.provider || existing.mode !== profile.mode) { return false; } if (existing.email && profile.email && existing.email !== profile.email) { return false; } return true; } function hasAuthProfileConfigConflict( config: OpenClawConfig, profile: CodexAuthProfileConfig, overwrite: boolean, ): boolean { if (overwrite) { return false; } const existing = config.auth?.profiles?.[profile.profileId]; return Boolean(existing && !existingAuthProfileConfigIsCompatible(existing, profile)); } function hasCurrentAuthProfileConfigConflict( ctx: MigrationProviderContext, profile: CodexAuthProfileConfig, ): boolean { let config = ctx.config; try { config = (resolveMigrationConfigRuntime(ctx)?.current?.() as OpenClawConfig | undefined) ?? config; } catch { // Fall back to the planning snapshot; direct config writes recheck inside mutate. } return hasAuthProfileConfigConflict(config, profile, Boolean(ctx.overwrite)); } function applyDefaultModelIfMissing(cfg: OpenClawConfig): OpenClawConfig { const currentModel = cfg.agents?.defaults?.model; const primary = typeof currentModel === "string" ? currentModel : isRecord(currentModel) ? readString(currentModel.primary) : undefined; if (primary) { return cfg; } return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, model: { ...(isRecord(currentModel) ? currentModel : {}), primary: OPENAI_CODEX_DEFAULT_MODEL, }, }, }, }; } function applyOAuthConfigToConfig( cfg: OpenClawConfig, credential: Extract, profileId: string, ): OpenClawConfig { let next = mergeMigrationConfigValue(cfg, credential.result.configPatch) as OpenClawConfig; const profile = credential.result.profiles[0]; if (profile) { next = applyAuthProfileConfig(next, { profileId, provider: profile.credential.provider, mode: "oauth", ...("email" in profile.credential && profile.credential.email ? { email: profile.credential.email } : {}), ...("displayName" in profile.credential && profile.credential.displayName ? { displayName: profile.credential.displayName } : {}), preferProfileFirst: false, }); } return applyDefaultModelIfMissing(next); } function applyApiKeyConfigToConfig( cfg: OpenClawConfig, credential: Extract, profileId: string, ): OpenClawConfig { return applyAuthProfileConfig(cfg, { profileId, provider: credential.provider, mode: "api_key", displayName: CODEX_IMPORT_DISPLAY_NAME, preferProfileFirst: false, }); } function shouldReturnAuthConfigPatch(ctx: MigrationProviderContext): boolean { return ctx.providerOptions?.configPatchMode === CODEX_CONFIG_PATCH_MODE_RETURN; } function authProfileConfigForCredential( credential: CodexAuthCredential, profileId: string, ): CodexAuthProfileConfig | null { if (credential.kind === "api_key") { return { profileId, provider: credential.provider, mode: "api_key", displayName: CODEX_IMPORT_DISPLAY_NAME, }; } const profile = credential.result.profiles[0]; if (!profile || profile.credential.type !== "oauth") { return null; } return { profileId, provider: profile.credential.provider, mode: "oauth", ...(profile.credential.email ? { email: profile.credential.email } : {}), ...(profile.credential.displayName ? { displayName: profile.credential.displayName } : {}), }; } async function applyCodexAuthProfileConfig( ctx: MigrationProviderContext, profile: CodexAuthProfileConfig, applyConfig: (config: OpenClawConfig) => OpenClawConfig, ): Promise { const configApi = resolveMigrationConfigRuntime(ctx); if (!configApi?.current || !configApi.mutateConfigFile) { return "unavailable"; } try { await configApi.mutateConfigFile({ base: "runtime", afterWrite: { mode: "auto" }, mutate(draft) { const current = draft; if (hasAuthProfileConfigConflict(current, profile, Boolean(ctx.overwrite))) { throw new CodexAuthConfigConflict(); } const next = applyConfig(current); replaceConfigDraft(draft, next); }, }); return "configured"; } catch (error) { return error instanceof CodexAuthConfigConflict ? "conflict" : "unavailable"; } } async function applyCodexAuthConfig( ctx: MigrationProviderContext, credential: CodexAuthCredential, profileId: string, ): Promise { const profile = authProfileConfigForCredential(credential, profileId); if (!profile) { return "unavailable"; } return applyCodexAuthProfileConfig(ctx, profile, (config) => applyCredentialConfig(config, credential, profileId), ); } function applyCredentialConfig( config: OpenClawConfig, credential: CodexAuthCredential, profileId: string, ): OpenClawConfig { return credential.kind === "oauth" ? applyOAuthConfigToConfig(config, credential, profileId) : applyApiKeyConfigToConfig(config, credential, profileId); } export async function buildCodexAuthItems(params: { ctx: MigrationProviderContext; source: CodexAuthSource; targets: CodexMigrationTargets; }): Promise { const credentials = await readCodexAuthCredentials(params.source); if (credentials.length === 0) { return []; } const store = loadAuthProfileStoreWithoutExternalProfiles(params.targets.agentDir); const skipped = !params.ctx.includeSecrets; return credentials.map((credential) => { const { profileId, matchedExisting } = itemProfileTarget(credential, store); const targetExists = Boolean(store.profiles[profileId]); const configProfile = authProfileConfigForCredential(credential, profileId); const configConflict = configProfile ? hasAuthProfileConfigConflict( params.ctx.config, configProfile, Boolean(params.ctx.overwrite), ) : false; const conflict = ((targetExists && !matchedExisting && !params.ctx.overwrite) || configConflict) && !skipped; return createMigrationItem({ id: `auth:${credential.provider}`, kind: "auth", action: skipped ? "skip" : "create", source: params.source.authPath, target: `${params.targets.agentDir}/auth-profiles.json#${profileId}`, status: skipped ? "skipped" : conflict ? "conflict" : "planned", sensitive: true, reason: skipped ? CODEX_REASON_AUTH_NOT_SELECTED : conflict ? CODEX_REASON_AUTH_PROFILE_EXISTS : undefined, message: credential.kind === "oauth" ? "Import Codex OAuth credentials and configure OpenAI Codex models." : "Import Codex OpenAI API key.", details: { provider: credential.provider, profileId, sourceProfileId: credential.profileId, sourceKind: "codex-auth-json", credentialKind: credential.kind, }, }); }); } export async function applyCodexAuthItems(params: { ctx: MigrationProviderContext; item: MigrationItem; source: CodexAuthSource; targets: CodexMigrationTargets; }): Promise { const { ctx, item, source, targets } = params; if (item.status !== "planned") { return [item]; } const profileId = typeof item.details?.profileId === "string" ? item.details.profileId : ""; const provider = typeof item.details?.provider === "string" ? item.details.provider : ""; const sourceProfileId = typeof item.details?.sourceProfileId === "string" ? item.details.sourceProfileId : undefined; if (!profileId || !provider) { return [markMigrationItemError(item, CODEX_REASON_MISSING_AUTH_METADATA)]; } const credential = (await readCodexAuthCredentials(source)).find( (candidate) => candidate.provider === provider, ); if (!credential) { return [markMigrationItemSkipped(item, CODEX_REASON_AUTH_NO_LONGER_PRESENT)]; } if (credential.kind === "oauth" && sourceProfileId && credential.profileId !== sourceProfileId) { return [markMigrationItemSkipped(item, CODEX_REASON_AUTH_NO_LONGER_PRESENT)]; } const oauthProfile = credential.kind === "oauth" ? credential.result.profiles[0] : undefined; const oauthCredential = oauthProfile?.credential.type === "oauth" ? oauthProfile.credential : undefined; if (credential.kind === "oauth" && !oauthCredential) { return [markMigrationItemError(item, CODEX_REASON_MISSING_AUTH_METADATA)]; } const configProfile = authProfileConfigForCredential(credential, profileId); if (!configProfile) { return [markMigrationItemError(item, CODEX_REASON_MISSING_AUTH_METADATA)]; } if (hasCurrentAuthProfileConfigConflict(ctx, configProfile)) { return [markMigrationItemConflict(item, CODEX_REASON_AUTH_PROFILE_EXISTS)]; } let conflicted = false; let wrote = false; const store = await updateAuthProfileStoreWithLock({ agentDir: targets.agentDir, stateDir: ctx.stateDir, updater: (freshStore) => { const existing = freshStore.profiles[profileId]; if (!ctx.overwrite && existing) { const matchedProfileId = credential.kind === "oauth" ? findMatchingOAuthProfile(freshStore, oauthCredential!) : findMatchingApiKeyProfile(freshStore, credential.provider, credential.key); if (matchedProfileId === profileId) { return false; } conflicted = true; return false; } freshStore.profiles[profileId] = credential.kind === "oauth" ? { ...oauthCredential!, displayName: CODEX_IMPORT_DISPLAY_NAME, } : { ...buildApiKeyCredential(credential.provider, credential.key), displayName: CODEX_IMPORT_DISPLAY_NAME, }; wrote = true; return true; }, }); if (conflicted) { return [markMigrationItemConflict(item, CODEX_REASON_AUTH_PROFILE_EXISTS)]; } if (!store?.profiles[profileId]) { return [markMigrationItemError(item, CODEX_REASON_AUTH_PROFILE_WRITE_FAILED)]; } const configResult = shouldReturnAuthConfigPatch(ctx) ? "unavailable" : await applyCodexAuthConfig(ctx, credential, profileId); if (configResult === "conflict") { return [markMigrationItemConflict(item, CODEX_REASON_AUTH_PROFILE_EXISTS)]; } const migratedItem: MigrationItem = { ...item, status: "migrated", details: { ...item.details, wroteAuthProfile: wrote, configUpdated: configResult === "configured", ...(shouldReturnAuthConfigPatch(ctx) ? { configPatchReturned: true } : {}), }, }; return [ migratedItem, ...(shouldReturnAuthConfigPatch(ctx) ? buildCodexAuthConfigPatchItems(ctx, migratedItem, credential, profileId) : []), ]; } function buildCodexAuthConfigPatchItems( ctx: MigrationProviderContext, item: MigrationItem, credential: CodexAuthCredential, profileId: string, ): MigrationItem[] { const next = applyCredentialConfig(ctx.config, credential, profileId); const items: MigrationItem[] = []; if (next.auth) { items.push( createMigrationItem({ id: `${item.id}:config:auth`, kind: "config", action: "merge", status: "migrated", target: "auth", message: "Configure imported Codex auth profile.", details: { path: ["auth"], value: next.auth, }, }), ); } if (next.agents?.defaults) { items.push( createMigrationItem({ id: `${item.id}:config:agents-defaults`, kind: "config", action: "merge", status: "migrated", target: "agents.defaults", message: "Configure imported Codex models.", details: { path: ["agents", "defaults"], value: next.agents.defaults, }, }), ); } return items; }