Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
897
extensions/codex/src/app-server/auth-bridge.ts
Normal file
897
extensions/codex/src/app-server/auth-bridge.ts
Normal file
@@ -0,0 +1,897 @@
|
||||
// Codex plugin module implements auth bridge behavior.
|
||||
import { createHash } from "node:crypto";
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
findPersistedAuthProfileCredential,
|
||||
loadAuthProfileStoreForSecretsRuntime,
|
||||
refreshOAuthCredentialForRuntime,
|
||||
resolveAuthProfileOrder,
|
||||
resolveProviderIdForAuth,
|
||||
resolveApiKeyForProfile,
|
||||
resolveDefaultAgentDir,
|
||||
resolvePersistedAuthProfileOwnerAgentDir,
|
||||
type AuthProfileCredential,
|
||||
type AuthProfileStore,
|
||||
type OAuthCredential,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { hasUsableOAuthCredential } from "openclaw/plugin-sdk/provider-auth";
|
||||
import type { CodexAppServerClient } from "./client.js";
|
||||
import { resolveCodexAppServerUserHomeDir, type CodexAppServerStartOptions } from "./config.js";
|
||||
import type {
|
||||
CodexChatgptAuthTokensRefreshResponse,
|
||||
CodexGetAccountResponse,
|
||||
CodexLoginAccountParams,
|
||||
} from "./protocol.js";
|
||||
import { resolveCodexAppServerSpawnEnv } from "./transport-stdio.js";
|
||||
|
||||
const CODEX_APP_SERVER_AUTH_PROVIDER = "openai";
|
||||
const OPENAI_CODEX_APP_SERVER_AUTH_PROVIDER = "openai-codex";
|
||||
const LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER = "codex-cli";
|
||||
const CODEX_APP_SERVER_EXTERNAL_CLI_PROVIDER_IDS = [
|
||||
CODEX_APP_SERVER_AUTH_PROVIDER,
|
||||
LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER,
|
||||
];
|
||||
const OPENAI_PROVIDER = "openai";
|
||||
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai:default";
|
||||
const CODEX_HOME_ENV_VAR = "CODEX_HOME";
|
||||
const HOME_ENV_VAR = "HOME";
|
||||
const CODEX_APP_SERVER_HOME_DIRNAME = "codex-home";
|
||||
const CODEX_APP_SERVER_NATIVE_HOME_DIRNAME = "home";
|
||||
const CODEX_API_KEY_ENV_VAR = "CODEX_API_KEY";
|
||||
const OPENAI_API_KEY_ENV_VAR = "OPENAI_API_KEY";
|
||||
const CODEX_APP_SERVER_API_KEY_ENV_VARS = [CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR];
|
||||
const CODEX_APP_SERVER_HOME_ENV_VARS = [CODEX_HOME_ENV_VAR, HOME_ENV_VAR];
|
||||
const CODEX_AUTH_JSON_FILENAME = "auth.json";
|
||||
const CODEX_HOME_DIRNAME = ".codex";
|
||||
|
||||
type AuthProfileOrderConfig = Parameters<typeof resolveAuthProfileOrder>[0]["cfg"];
|
||||
const scopedOAuthRefreshQueues = new WeakMap<
|
||||
AuthProfileStore,
|
||||
Map<string, Promise<OAuthCredential>>
|
||||
>();
|
||||
|
||||
export async function bridgeCodexAppServerStartOptions(params: {
|
||||
startOptions: CodexAppServerStartOptions;
|
||||
agentDir: string;
|
||||
authProfileId?: string | null;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<CodexAppServerStartOptions> {
|
||||
if (params.startOptions.transport !== "stdio") {
|
||||
return params.startOptions;
|
||||
}
|
||||
const scopedStartOptions = await withCodexHomeEnvironment(params.startOptions, params.agentDir);
|
||||
if (params.authProfileId === null) {
|
||||
return scopedStartOptions;
|
||||
}
|
||||
const store = resolveCodexAppServerAuthProfileStore({
|
||||
agentDir: params.agentDir,
|
||||
authProfileId: params.authProfileId,
|
||||
authProfileStore: params.authProfileStore,
|
||||
config: params.config,
|
||||
});
|
||||
const authProfileId = resolveCodexAppServerAuthProfileId({
|
||||
authProfileId: params.authProfileId,
|
||||
store,
|
||||
config: params.config,
|
||||
});
|
||||
const shouldClearInheritedOpenAiApiKey = shouldClearOpenAiApiKeyForCodexAuthProfile({
|
||||
store,
|
||||
authProfileId,
|
||||
config: params.config,
|
||||
});
|
||||
return shouldClearInheritedOpenAiApiKey
|
||||
? withClearedEnvironmentVariables(scopedStartOptions, CODEX_APP_SERVER_API_KEY_ENV_VARS)
|
||||
: scopedStartOptions;
|
||||
}
|
||||
|
||||
export function resolveCodexAppServerAuthProfileId(params: {
|
||||
authProfileId?: string;
|
||||
store: ReturnType<typeof ensureAuthProfileStore>;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): string | undefined {
|
||||
const requested = params.authProfileId?.trim();
|
||||
if (requested) {
|
||||
return requested;
|
||||
}
|
||||
return resolveAuthProfileOrder({
|
||||
cfg: params.config,
|
||||
store: params.store,
|
||||
provider: CODEX_APP_SERVER_AUTH_PROVIDER,
|
||||
})[0]?.trim();
|
||||
}
|
||||
|
||||
export function resolveCodexAppServerAuthProfileIdForAgent(params: {
|
||||
authProfileId?: string;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): string | undefined {
|
||||
const agentDir = params.agentDir?.trim() || resolveDefaultAgentDir(params.config ?? {});
|
||||
const store = resolveCodexAppServerAuthProfileStore({
|
||||
agentDir,
|
||||
authProfileId: params.authProfileId,
|
||||
authProfileStore: params.authProfileStore,
|
||||
config: params.config,
|
||||
});
|
||||
return resolveCodexAppServerAuthProfileId({
|
||||
authProfileId: params.authProfileId,
|
||||
store,
|
||||
config: params.config,
|
||||
});
|
||||
}
|
||||
|
||||
function ensureCodexAppServerAuthProfileStore(params: {
|
||||
agentDir?: string;
|
||||
authProfileId?: string;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): ReturnType<typeof ensureAuthProfileStore> {
|
||||
return ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
config: params.config,
|
||||
externalCliProviderIds: CODEX_APP_SERVER_EXTERNAL_CLI_PROVIDER_IDS,
|
||||
...(params.authProfileId ? { externalCliProfileIds: [params.authProfileId] } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveCodexAppServerAuthProfileStore(params: {
|
||||
agentDir?: string;
|
||||
authProfileId?: string;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): AuthProfileStore {
|
||||
if (params.authProfileStore) {
|
||||
const providedProfileId = resolveCodexAppServerAuthProfileId({
|
||||
authProfileId: params.authProfileId,
|
||||
store: params.authProfileStore,
|
||||
config: params.config,
|
||||
});
|
||||
if (providedProfileId && params.authProfileStore.profiles[providedProfileId]) {
|
||||
return params.authProfileStore;
|
||||
}
|
||||
}
|
||||
const overlaidStore = ensureCodexAppServerAuthProfileStore({
|
||||
agentDir: params.agentDir,
|
||||
authProfileId: params.authProfileId,
|
||||
config: params.config,
|
||||
});
|
||||
if (!params.authProfileStore) {
|
||||
return overlaidStore;
|
||||
}
|
||||
const order =
|
||||
params.authProfileStore.order || overlaidStore.order
|
||||
? {
|
||||
...overlaidStore.order,
|
||||
...params.authProfileStore.order,
|
||||
}
|
||||
: undefined;
|
||||
const profiles = {
|
||||
...overlaidStore.profiles,
|
||||
...params.authProfileStore.profiles,
|
||||
};
|
||||
const suppliedProfileIds = new Set(Object.keys(params.authProfileStore.profiles));
|
||||
const mergeRuntimeProfileIds = (overlaidIds?: string[], suppliedIds?: string[]) => [
|
||||
...(overlaidIds ?? []).filter((profileId) => !suppliedProfileIds.has(profileId)),
|
||||
...(suppliedIds ?? []),
|
||||
];
|
||||
const runtimePersistedProfileIds = mergeRuntimeProfileIds(
|
||||
overlaidStore.runtimePersistedProfileIds,
|
||||
params.authProfileStore.runtimePersistedProfileIds,
|
||||
).filter((profileId) => profiles[profileId]);
|
||||
const runtimeExternalProfileIds = mergeRuntimeProfileIds(
|
||||
overlaidStore.runtimeExternalProfileIds,
|
||||
params.authProfileStore.runtimeExternalProfileIds,
|
||||
).filter((profileId) => profiles[profileId]);
|
||||
const runtimeExternalProfileIdsAuthoritative =
|
||||
overlaidStore.runtimeExternalProfileIdsAuthoritative === true ||
|
||||
params.authProfileStore.runtimeExternalProfileIdsAuthoritative === true;
|
||||
return {
|
||||
...params.authProfileStore,
|
||||
...(order ? { order } : {}),
|
||||
profiles,
|
||||
...(runtimePersistedProfileIds.length > 0
|
||||
? { runtimePersistedProfileIds: [...new Set(runtimePersistedProfileIds)] }
|
||||
: {}),
|
||||
...(runtimeExternalProfileIds.length > 0 || runtimeExternalProfileIdsAuthoritative
|
||||
? {
|
||||
runtimeExternalProfileIds: [...new Set(runtimeExternalProfileIds)],
|
||||
...(runtimeExternalProfileIdsAuthoritative
|
||||
? { runtimeExternalProfileIdsAuthoritative: true }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveCodexAppServerAuthAccountCacheKey(params: {
|
||||
authProfileId?: string;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<string | undefined> {
|
||||
const agentDir = params.agentDir?.trim() || resolveDefaultAgentDir(params.config ?? {});
|
||||
const store = resolveCodexAppServerAuthProfileStore({
|
||||
agentDir,
|
||||
authProfileId: params.authProfileId,
|
||||
authProfileStore: params.authProfileStore,
|
||||
config: params.config,
|
||||
});
|
||||
const profileId = resolveCodexAppServerAuthProfileId({
|
||||
authProfileId: params.authProfileId,
|
||||
store,
|
||||
config: params.config,
|
||||
});
|
||||
if (!profileId) {
|
||||
return undefined;
|
||||
}
|
||||
const credential = store.profiles[profileId];
|
||||
if (!credential || !isCodexAppServerAuthProfileCredential(credential, params.config)) {
|
||||
return undefined;
|
||||
}
|
||||
if (credential.type === "api_key") {
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
store,
|
||||
profileId,
|
||||
agentDir,
|
||||
});
|
||||
const apiKey = resolved?.apiKey?.trim();
|
||||
return apiKey
|
||||
? `${resolveChatgptAccountId(profileId, credential)}:${fingerprintApiKeyAuthProfileCacheKey(apiKey)}`
|
||||
: resolveChatgptAccountId(profileId, credential);
|
||||
}
|
||||
if (credential.type === "token") {
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
store,
|
||||
profileId,
|
||||
agentDir,
|
||||
});
|
||||
const accessToken = resolved?.apiKey?.trim();
|
||||
return accessToken
|
||||
? `${resolveChatgptAccountId(profileId, credential)}:${fingerprintTokenAuthProfileCacheKey(accessToken)}`
|
||||
: resolveChatgptAccountId(profileId, credential);
|
||||
}
|
||||
return resolveChatgptAccountId(profileId, credential);
|
||||
}
|
||||
|
||||
export function resolveCodexAppServerEnvApiKeyCacheKey(params: {
|
||||
startOptions: Pick<CodexAppServerStartOptions, "transport" | "env" | "clearEnv">;
|
||||
baseEnv?: NodeJS.ProcessEnv;
|
||||
platform?: NodeJS.Platform;
|
||||
}): string | undefined {
|
||||
if (params.startOptions.transport !== "stdio") {
|
||||
return undefined;
|
||||
}
|
||||
const env = resolveCodexAppServerSpawnEnv(
|
||||
params.startOptions,
|
||||
params.baseEnv ?? process.env,
|
||||
params.platform ?? process.platform,
|
||||
);
|
||||
const apiKey = readFirstNonEmptyEnvEntry(env, CODEX_APP_SERVER_API_KEY_ENV_VARS);
|
||||
if (!apiKey) {
|
||||
return undefined;
|
||||
}
|
||||
const hash = createHash("sha256");
|
||||
hash.update("openclaw:codex:app-server-env-api-key:v1");
|
||||
hash.update("\0");
|
||||
hash.update(apiKey.key);
|
||||
hash.update("\0");
|
||||
hash.update(apiKey.value);
|
||||
return `${apiKey.key}:sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
export function resolveCodexAppServerFallbackApiKeyCacheKey(params: {
|
||||
startOptions: Pick<CodexAppServerStartOptions, "transport" | "env" | "clearEnv">;
|
||||
baseEnv?: NodeJS.ProcessEnv;
|
||||
platform?: NodeJS.Platform;
|
||||
}): string | undefined {
|
||||
if (params.startOptions.transport !== "stdio") {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
resolveCodexAppServerEnvApiKeyCacheKey(params) ??
|
||||
resolveCodexCliAuthFileApiKeyCacheKey(params.baseEnv ?? process.env)
|
||||
);
|
||||
}
|
||||
|
||||
function fingerprintApiKeyAuthProfileCacheKey(apiKey: string): string {
|
||||
const hash = createHash("sha256");
|
||||
hash.update("openclaw:codex:app-server-auth-profile-api-key:v1");
|
||||
hash.update("\0");
|
||||
hash.update(apiKey);
|
||||
return `api_key:sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
function fingerprintTokenAuthProfileCacheKey(accessToken: string): string {
|
||||
const hash = createHash("sha256");
|
||||
hash.update("openclaw:codex:app-server-auth-profile-token:v1");
|
||||
hash.update("\0");
|
||||
hash.update(accessToken);
|
||||
return `token:sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
function fingerprintCodexCliAuthFileApiKeyCacheKey(apiKey: string): string {
|
||||
const hash = createHash("sha256");
|
||||
hash.update("openclaw:codex:app-server-cli-auth-json-api-key:v1");
|
||||
hash.update("\0");
|
||||
hash.update(apiKey);
|
||||
return `CODEX_AUTH_JSON:sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
export function resolveCodexAppServerHomeDir(agentDir: string): string {
|
||||
return path.join(path.resolve(agentDir), CODEX_APP_SERVER_HOME_DIRNAME);
|
||||
}
|
||||
|
||||
export function resolveCodexAppServerNativeHomeDir(agentDir: string): string {
|
||||
return path.join(resolveCodexAppServerHomeDir(agentDir), CODEX_APP_SERVER_NATIVE_HOME_DIRNAME);
|
||||
}
|
||||
|
||||
async function withCodexHomeEnvironment(
|
||||
startOptions: CodexAppServerStartOptions,
|
||||
agentDir: string,
|
||||
): Promise<CodexAppServerStartOptions> {
|
||||
const codexHome = startOptions.env?.[CODEX_HOME_ENV_VAR]?.trim()
|
||||
? startOptions.env[CODEX_HOME_ENV_VAR]
|
||||
: startOptions.homeScope === "user"
|
||||
? resolveCodexAppServerUserHomeDir(process.env)
|
||||
: resolveCodexAppServerHomeDir(agentDir);
|
||||
const nativeHome = startOptions.env?.[HOME_ENV_VAR]?.trim()
|
||||
? startOptions.env[HOME_ENV_VAR]
|
||||
: undefined;
|
||||
await fs.mkdir(codexHome, { recursive: true });
|
||||
if (nativeHome) {
|
||||
await fs.mkdir(nativeHome, { recursive: true });
|
||||
}
|
||||
const nextStartOptions: CodexAppServerStartOptions = {
|
||||
...startOptions,
|
||||
env: {
|
||||
...startOptions.env,
|
||||
[CODEX_HOME_ENV_VAR]: codexHome,
|
||||
...(nativeHome ? { [HOME_ENV_VAR]: nativeHome } : {}),
|
||||
},
|
||||
};
|
||||
const clearEnv = withoutClearedCodexHomeEnv(startOptions.clearEnv);
|
||||
if (clearEnv) {
|
||||
nextStartOptions.clearEnv = clearEnv;
|
||||
} else {
|
||||
delete nextStartOptions.clearEnv;
|
||||
}
|
||||
return nextStartOptions;
|
||||
}
|
||||
|
||||
function withoutClearedCodexHomeEnv(clearEnv: string[] | undefined): string[] | undefined {
|
||||
if (!clearEnv) {
|
||||
return undefined;
|
||||
}
|
||||
const reserved = new Set(CODEX_APP_SERVER_HOME_ENV_VARS);
|
||||
const filtered = clearEnv.filter((envVar) => !reserved.has(envVar.trim().toUpperCase()));
|
||||
return filtered.length === clearEnv.length ? clearEnv : filtered;
|
||||
}
|
||||
|
||||
export async function applyCodexAppServerAuthProfile(params: {
|
||||
client: CodexAppServerClient;
|
||||
agentDir: string;
|
||||
authProfileId?: string | null;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<void> {
|
||||
if (params.authProfileId === null) {
|
||||
return;
|
||||
}
|
||||
const loginParams = await resolveCodexAppServerAuthProfileLoginParams({
|
||||
agentDir: params.agentDir,
|
||||
authProfileId: params.authProfileId,
|
||||
authProfileStore: params.authProfileStore,
|
||||
config: params.config,
|
||||
});
|
||||
if (!loginParams) {
|
||||
if (params.startOptions?.transport !== "stdio") {
|
||||
return;
|
||||
}
|
||||
const env = resolveCodexAppServerSpawnEnv(params.startOptions, process.env);
|
||||
const fallbackLoginParams = await resolveCodexAppServerFallbackApiKeyLoginParams({
|
||||
client: params.client,
|
||||
env,
|
||||
codexCliAuthEnv: process.env,
|
||||
});
|
||||
if (fallbackLoginParams) {
|
||||
await params.client.request("account/login/start", fallbackLoginParams);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await params.client.request("account/login/start", loginParams);
|
||||
}
|
||||
|
||||
function resolveCodexAppServerAuthProfileLoginParams(params: {
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<CodexLoginAccountParams | undefined> {
|
||||
return resolveCodexAppServerAuthProfileLoginParamsInternal(params);
|
||||
}
|
||||
|
||||
export async function refreshCodexAppServerAuthTokens(params: {
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<CodexChatgptAuthTokensRefreshResponse> {
|
||||
const loginParams = await resolveCodexAppServerAuthProfileLoginParamsInternal({
|
||||
...params,
|
||||
forceOAuthRefresh: true,
|
||||
});
|
||||
if (!loginParams || loginParams.type !== "chatgptAuthTokens") {
|
||||
throw new Error("Codex app-server ChatGPT token refresh requires an OAuth auth profile.");
|
||||
}
|
||||
return {
|
||||
accessToken: loginParams.accessToken,
|
||||
chatgptAccountId: loginParams.chatgptAccountId,
|
||||
chatgptPlanType: loginParams.chatgptPlanType ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: {
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
forceOAuthRefresh?: boolean;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<CodexLoginAccountParams | undefined> {
|
||||
const store = resolveCodexAppServerAuthProfileStore({
|
||||
agentDir: params.agentDir,
|
||||
authProfileId: params.authProfileId,
|
||||
authProfileStore: params.authProfileStore,
|
||||
config: params.config,
|
||||
});
|
||||
const profileId = resolveCodexAppServerAuthProfileId({
|
||||
authProfileId: params.authProfileId,
|
||||
store,
|
||||
config: params.config,
|
||||
});
|
||||
if (!profileId) {
|
||||
return undefined;
|
||||
}
|
||||
const credential = store.profiles[profileId];
|
||||
if (!credential) {
|
||||
throw new Error(`Codex app-server auth profile "${profileId}" was not found.`);
|
||||
}
|
||||
if (!isCodexAppServerAuthProfileCredential(credential, params.config)) {
|
||||
throw new Error(
|
||||
`Codex app-server auth profile "${profileId}" must be OpenAI Codex auth or an OpenAI API-key backup.`,
|
||||
);
|
||||
}
|
||||
const loginParams = await resolveLoginParamsForCredential(profileId, credential, {
|
||||
agentDir: params.agentDir,
|
||||
store,
|
||||
preferStoreCredential: Boolean(params.authProfileStore?.profiles[profileId]),
|
||||
forceOAuthRefresh: params.forceOAuthRefresh === true,
|
||||
config: params.config,
|
||||
});
|
||||
if (!loginParams) {
|
||||
throw new Error(
|
||||
`Codex app-server auth profile "${profileId}" does not contain usable credentials.`,
|
||||
);
|
||||
}
|
||||
return loginParams;
|
||||
}
|
||||
|
||||
async function resolveCodexAppServerFallbackApiKeyLoginParams(params: {
|
||||
client: CodexAppServerClient;
|
||||
env: NodeJS.ProcessEnv;
|
||||
codexCliAuthEnv: NodeJS.ProcessEnv;
|
||||
}): Promise<CodexLoginAccountParams | undefined> {
|
||||
const apiKey =
|
||||
readFirstNonEmptyEnv(params.env, CODEX_APP_SERVER_API_KEY_ENV_VARS) ??
|
||||
(await readCodexCliAuthFileApiKey(params.codexCliAuthEnv));
|
||||
if (!apiKey) {
|
||||
return undefined;
|
||||
}
|
||||
const response = await params.client.request<CodexGetAccountResponse>("account/read", {
|
||||
refreshToken: false,
|
||||
});
|
||||
if (response.account) {
|
||||
return undefined;
|
||||
}
|
||||
return { type: "apiKey", apiKey };
|
||||
}
|
||||
|
||||
function resolveCodexCliAuthFilePath(env: NodeJS.ProcessEnv): string {
|
||||
const configuredCodexHome = env[CODEX_HOME_ENV_VAR]?.trim();
|
||||
if (configuredCodexHome) {
|
||||
return path.join(resolveHomeRelativePath(configuredCodexHome, env), CODEX_AUTH_JSON_FILENAME);
|
||||
}
|
||||
const home = env[HOME_ENV_VAR]?.trim() || env.USERPROFILE?.trim() || os.homedir();
|
||||
return path.join(home, CODEX_HOME_DIRNAME, CODEX_AUTH_JSON_FILENAME);
|
||||
}
|
||||
|
||||
function resolveHomeRelativePath(value: string, env: NodeJS.ProcessEnv): string {
|
||||
if (value === "~" || value.startsWith("~/") || value.startsWith("~\\")) {
|
||||
const home = env[HOME_ENV_VAR]?.trim() || env.USERPROFILE?.trim() || os.homedir();
|
||||
return path.join(home, value.slice(value === "~" ? 1 : 2));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseCodexCliAuthFileApiKey(raw: string): string | undefined {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const apiKey = (parsed as Record<string, unknown>).OPENAI_API_KEY;
|
||||
return typeof apiKey === "string" && apiKey.trim() ? apiKey.trim() : undefined;
|
||||
}
|
||||
|
||||
async function readCodexCliAuthFileApiKey(env: NodeJS.ProcessEnv): Promise<string | undefined> {
|
||||
try {
|
||||
return parseCodexCliAuthFileApiKey(await fs.readFile(resolveCodexCliAuthFilePath(env), "utf8"));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCodexCliAuthFileApiKeyCacheKey(env: NodeJS.ProcessEnv): string | undefined {
|
||||
try {
|
||||
const apiKey = parseCodexCliAuthFileApiKey(
|
||||
fsSync.readFileSync(resolveCodexCliAuthFilePath(env), "utf8"),
|
||||
);
|
||||
return apiKey ? fingerprintCodexCliAuthFileApiKeyCacheKey(apiKey) : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveLoginParamsForCredential(
|
||||
profileId: string,
|
||||
credential: AuthProfileCredential,
|
||||
params: {
|
||||
agentDir: string;
|
||||
store: AuthProfileStore;
|
||||
preferStoreCredential: boolean;
|
||||
forceOAuthRefresh: boolean;
|
||||
config?: AuthProfileOrderConfig;
|
||||
},
|
||||
): Promise<CodexLoginAccountParams | undefined> {
|
||||
// Runtime honors the persisted auth profile type. Shape-based remediation
|
||||
// belongs at credential entry time so request handling does not preemptively
|
||||
// reject opaque provider credentials.
|
||||
if (credential.type === "api_key") {
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
store: params.preferStoreCredential
|
||||
? params.store
|
||||
: ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }),
|
||||
profileId,
|
||||
agentDir: params.agentDir,
|
||||
});
|
||||
const apiKey = resolved?.apiKey?.trim();
|
||||
return apiKey ? { type: "apiKey", apiKey } : undefined;
|
||||
}
|
||||
if (credential.type === "token") {
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
store: params.preferStoreCredential
|
||||
? params.store
|
||||
: ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }),
|
||||
profileId,
|
||||
agentDir: params.agentDir,
|
||||
});
|
||||
const accessToken = resolved?.apiKey?.trim();
|
||||
return accessToken
|
||||
? buildChatgptAuthTokensParams(profileId, credential, accessToken)
|
||||
: undefined;
|
||||
}
|
||||
if (credential.type !== "oauth") {
|
||||
return undefined;
|
||||
}
|
||||
const resolvedCredential = await resolveOAuthCredentialForCodexAppServer(profileId, credential, {
|
||||
agentDir: params.agentDir,
|
||||
store: params.store,
|
||||
preferStoreCredential: params.preferStoreCredential,
|
||||
forceRefresh: params.forceOAuthRefresh,
|
||||
config: params.config,
|
||||
});
|
||||
const accessToken = resolvedCredential.access?.trim();
|
||||
return accessToken
|
||||
? buildChatgptAuthTokensParams(profileId, resolvedCredential, accessToken)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function resolveOAuthCredentialForCodexAppServer(
|
||||
profileId: string,
|
||||
credential: OAuthCredential,
|
||||
params: {
|
||||
agentDir: string;
|
||||
store: AuthProfileStore;
|
||||
preferStoreCredential: boolean;
|
||||
forceRefresh: boolean;
|
||||
config?: AuthProfileOrderConfig;
|
||||
},
|
||||
): Promise<OAuthCredential> {
|
||||
const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir({
|
||||
agentDir: params.agentDir,
|
||||
profileId,
|
||||
});
|
||||
const persistedCredential = findPersistedAuthProfileCredential({
|
||||
agentDir: ownerAgentDir,
|
||||
profileId,
|
||||
});
|
||||
const useScopedCredential =
|
||||
params.preferStoreCredential &&
|
||||
shouldUseScopedOAuthCredential({
|
||||
store: params.store,
|
||||
profileId,
|
||||
persistedCredential,
|
||||
suppliedCredential: credential,
|
||||
config: params.config,
|
||||
});
|
||||
const store = useScopedCredential
|
||||
? params.store
|
||||
: ensureCodexAppServerAuthProfileStore({
|
||||
agentDir: ownerAgentDir,
|
||||
authProfileId: profileId,
|
||||
config: params.config,
|
||||
});
|
||||
const persistedOAuthCredential =
|
||||
!useScopedCredential &&
|
||||
persistedCredential?.type === "oauth" &&
|
||||
isCodexAppServerAuthProvider(persistedCredential.provider, params.config)
|
||||
? persistedCredential
|
||||
: undefined;
|
||||
const ownerCredential = store.profiles[profileId];
|
||||
const overlaidOAuthCredential =
|
||||
ownerCredential?.type === "oauth" &&
|
||||
isCodexAppServerAuthProvider(ownerCredential.provider, params.config)
|
||||
? ownerCredential
|
||||
: undefined;
|
||||
if (useScopedCredential && overlaidOAuthCredential) {
|
||||
return await resolveScopedOAuthCredential({
|
||||
store,
|
||||
profileId,
|
||||
credential: overlaidOAuthCredential,
|
||||
forceRefresh: params.forceRefresh,
|
||||
});
|
||||
}
|
||||
if (params.forceRefresh && !persistedOAuthCredential && overlaidOAuthCredential) {
|
||||
const refreshedRuntimeCredential = await refreshOAuthCredentialForRuntime({
|
||||
credential: overlaidOAuthCredential,
|
||||
});
|
||||
if (!refreshedRuntimeCredential?.access?.trim()) {
|
||||
throw new Error(`Codex app-server auth profile "${profileId}" could not refresh.`);
|
||||
}
|
||||
store.profiles[profileId] = refreshedRuntimeCredential;
|
||||
return refreshedRuntimeCredential;
|
||||
}
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
store,
|
||||
profileId,
|
||||
agentDir: ownerAgentDir,
|
||||
forceRefresh: params.forceRefresh && Boolean(persistedOAuthCredential),
|
||||
});
|
||||
const refreshed = useScopedCredential
|
||||
? undefined
|
||||
: loadAuthProfileStoreForSecretsRuntime(ownerAgentDir).profiles[profileId];
|
||||
const refreshedOAuthCredential =
|
||||
refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider, params.config)
|
||||
? refreshed
|
||||
: undefined;
|
||||
if (refreshedOAuthCredential && isDeepStrictEqual(params.store.profiles[profileId], credential)) {
|
||||
// Persisted refreshes rotate refresh tokens. Keep an isolated prepared
|
||||
// store aligned without reverting a concurrent caller-owned replacement.
|
||||
params.store.profiles[profileId] = refreshedOAuthCredential;
|
||||
}
|
||||
const storedCredential = store.profiles[profileId];
|
||||
const candidate = refreshedOAuthCredential
|
||||
? refreshedOAuthCredential
|
||||
: storedCredential?.type === "oauth" &&
|
||||
isCodexAppServerAuthProvider(storedCredential.provider, params.config)
|
||||
? storedCredential
|
||||
: credential;
|
||||
return resolved?.apiKey ? { ...candidate, access: resolved.apiKey } : candidate;
|
||||
}
|
||||
|
||||
function shouldUseScopedOAuthCredential(params: {
|
||||
store: AuthProfileStore;
|
||||
profileId: string;
|
||||
persistedCredential: AuthProfileCredential | undefined;
|
||||
suppliedCredential: OAuthCredential;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): boolean {
|
||||
if (!params.store.runtimePersistedProfileIds?.includes(params.profileId)) {
|
||||
return true;
|
||||
}
|
||||
const persisted = params.persistedCredential;
|
||||
if (persisted?.type !== "oauth") {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
resolveProviderIdForAuth(persisted.provider, { config: params.config }) !==
|
||||
resolveProviderIdForAuth(params.suppliedCredential.provider, { config: params.config })
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
!isDeepStrictEqual(persisted, params.suppliedCredential) &&
|
||||
!hasMatchingOAuthIdentity(persisted, params.suppliedCredential)
|
||||
);
|
||||
}
|
||||
|
||||
function hasMatchingOAuthIdentity(persisted: OAuthCredential, supplied: OAuthCredential): boolean {
|
||||
const persistedAccountId = persisted.accountId?.trim();
|
||||
const suppliedAccountId = supplied.accountId?.trim();
|
||||
if (persistedAccountId && suppliedAccountId) {
|
||||
return persistedAccountId === suppliedAccountId;
|
||||
}
|
||||
const persistedEmail = persisted.email?.trim().toLowerCase();
|
||||
const suppliedEmail = supplied.email?.trim().toLowerCase();
|
||||
return Boolean(persistedEmail && suppliedEmail && persistedEmail === suppliedEmail);
|
||||
}
|
||||
|
||||
async function resolveScopedOAuthCredential(params: {
|
||||
store: AuthProfileStore;
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
forceRefresh: boolean;
|
||||
}): Promise<OAuthCredential> {
|
||||
const existingRefresh = scopedOAuthRefreshQueues.get(params.store)?.get(params.profileId);
|
||||
if (existingRefresh) {
|
||||
return await existingRefresh;
|
||||
}
|
||||
if (!params.forceRefresh && hasUsableOAuthCredential(params.credential)) {
|
||||
return params.credential;
|
||||
}
|
||||
|
||||
const storeRefreshes = scopedOAuthRefreshQueues.get(params.store) ?? new Map();
|
||||
scopedOAuthRefreshQueues.set(params.store, storeRefreshes);
|
||||
const refresh = (async () => {
|
||||
const current = params.store.profiles[params.profileId];
|
||||
const credential = current?.type === "oauth" ? current : params.credential;
|
||||
if (!params.forceRefresh && hasUsableOAuthCredential(credential)) {
|
||||
return credential;
|
||||
}
|
||||
const refreshed = await refreshOAuthCredentialForRuntime({ credential });
|
||||
if (!refreshed?.access?.trim()) {
|
||||
throw new Error(`Codex app-server auth profile "${params.profileId}" could not refresh.`);
|
||||
}
|
||||
if (!isDeepStrictEqual(params.store.profiles[params.profileId], credential)) {
|
||||
throw new Error(
|
||||
`Codex app-server auth profile "${params.profileId}" changed while refreshing.`,
|
||||
);
|
||||
}
|
||||
params.store.profiles[params.profileId] = refreshed;
|
||||
return refreshed;
|
||||
})();
|
||||
storeRefreshes.set(params.profileId, refresh);
|
||||
try {
|
||||
return await refresh;
|
||||
} finally {
|
||||
// Scoped stores are process-local; serialize their rotating refresh token
|
||||
// and release the queue entry with the refresh that owns it.
|
||||
if (storeRefreshes.get(params.profileId) === refresh) {
|
||||
storeRefreshes.delete(params.profileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isCodexAppServerAuthProvider(provider: string, config?: AuthProfileOrderConfig): boolean {
|
||||
const resolvedProvider = resolveProviderIdForAuth(provider, { config });
|
||||
return (
|
||||
resolvedProvider === CODEX_APP_SERVER_AUTH_PROVIDER ||
|
||||
resolvedProvider === OPENAI_CODEX_APP_SERVER_AUTH_PROVIDER ||
|
||||
// Older Codex auth profiles stored the CLI runtime id here. The app-server
|
||||
// login protocol still receives the same externally managed ChatGPT token.
|
||||
resolvedProvider === LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER
|
||||
);
|
||||
}
|
||||
|
||||
function isOpenAIApiKeyBackupCredential(
|
||||
credential: AuthProfileCredential,
|
||||
config?: AuthProfileOrderConfig,
|
||||
): boolean {
|
||||
return (
|
||||
credential.type === "api_key" &&
|
||||
resolveProviderIdForAuth(credential.provider, { config }) === OPENAI_PROVIDER
|
||||
);
|
||||
}
|
||||
|
||||
function isCodexAppServerAuthProfileCredential(
|
||||
credential: AuthProfileCredential,
|
||||
config?: AuthProfileOrderConfig,
|
||||
): boolean {
|
||||
return (
|
||||
isCodexAppServerAuthProvider(credential.provider, config) ||
|
||||
isOpenAIApiKeyBackupCredential(credential, config)
|
||||
);
|
||||
}
|
||||
|
||||
function shouldClearOpenAiApiKeyForCodexAuthProfile(params: {
|
||||
store: ReturnType<typeof ensureAuthProfileStore>;
|
||||
authProfileId?: string;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): boolean {
|
||||
const profileId = params.authProfileId?.trim();
|
||||
const credential = profileId
|
||||
? params.store.profiles[profileId]
|
||||
: params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID];
|
||||
return isCodexSubscriptionCredential(credential, params.config);
|
||||
}
|
||||
|
||||
function isCodexSubscriptionCredential(
|
||||
credential: AuthProfileCredential | undefined,
|
||||
config?: AuthProfileOrderConfig,
|
||||
): boolean {
|
||||
if (!credential || !isCodexAppServerAuthProvider(credential.provider, config)) {
|
||||
return false;
|
||||
}
|
||||
return credential.type === "oauth" || credential.type === "token";
|
||||
}
|
||||
|
||||
function withClearedEnvironmentVariables(
|
||||
startOptions: CodexAppServerStartOptions,
|
||||
envVars: readonly string[],
|
||||
): CodexAppServerStartOptions {
|
||||
const clearEnv = startOptions.clearEnv ?? [];
|
||||
const missingEnvVars = envVars.filter((envVar) => !clearEnv.includes(envVar));
|
||||
if (missingEnvVars.length === 0) {
|
||||
return startOptions;
|
||||
}
|
||||
return {
|
||||
...startOptions,
|
||||
clearEnv: [...clearEnv, ...missingEnvVars],
|
||||
};
|
||||
}
|
||||
|
||||
function readFirstNonEmptyEnv(env: NodeJS.ProcessEnv, keys: readonly string[]): string | undefined {
|
||||
return readFirstNonEmptyEnvEntry(env, keys)?.value;
|
||||
}
|
||||
|
||||
function readFirstNonEmptyEnvEntry(
|
||||
env: NodeJS.ProcessEnv,
|
||||
keys: readonly string[],
|
||||
): { key: string; value: string } | undefined {
|
||||
for (const key of keys) {
|
||||
const value = env[key]?.trim();
|
||||
if (value) {
|
||||
return { key, value };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildChatgptAuthTokensParams(
|
||||
profileId: string,
|
||||
credential: AuthProfileCredential,
|
||||
accessToken: string,
|
||||
): CodexLoginAccountParams {
|
||||
return {
|
||||
type: "chatgptAuthTokens",
|
||||
accessToken,
|
||||
chatgptAccountId: resolveChatgptAccountId(profileId, credential),
|
||||
chatgptPlanType: resolveChatgptPlanType(credential),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveChatgptPlanType(credential: AuthProfileCredential): string | null {
|
||||
const record = credential as Record<string, unknown>;
|
||||
const planType = record.chatgptPlanType ?? record.planType;
|
||||
return typeof planType === "string" && planType.trim() ? planType.trim() : null;
|
||||
}
|
||||
|
||||
function resolveChatgptAccountId(profileId: string, credential: AuthProfileCredential): string {
|
||||
if ("accountId" in credential && typeof credential.accountId === "string") {
|
||||
const accountId = credential.accountId.trim();
|
||||
if (accountId) {
|
||||
return accountId;
|
||||
}
|
||||
}
|
||||
const email = credential.email?.trim();
|
||||
return email || profileId;
|
||||
}
|
||||
Reference in New Issue
Block a user