fix(auth): dedupe codex-cli profiles
Co-authored-by: Oliver Drobnik <oliver@cocoanetics.com>
This commit is contained in:
@@ -58,6 +58,26 @@ function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: nu
|
||||
return cred.expires > now + EXTERNAL_CLI_NEAR_EXPIRY_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any existing openai-codex profile (other than codex-cli) that has the same
|
||||
* access and refresh tokens. This prevents creating a duplicate codex-cli profile
|
||||
* when the user has already set up a custom profile with the same credentials.
|
||||
*/
|
||||
export function findDuplicateCodexProfile(
|
||||
store: AuthProfileStore,
|
||||
creds: OAuthCredential,
|
||||
): string | undefined {
|
||||
for (const [profileId, profile] of Object.entries(store.profiles)) {
|
||||
if (profileId === CODEX_CLI_PROFILE_ID) continue;
|
||||
if (profile.type !== "oauth") continue;
|
||||
if (profile.provider !== "openai-codex") continue;
|
||||
if (profile.access === creds.access && profile.refresh === creds.refresh) {
|
||||
return profileId;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync OAuth credentials from external CLI tools (Claude Code CLI, Codex CLI) into the store.
|
||||
* This allows clawdbot to use the same credentials as these tools without requiring
|
||||
@@ -143,31 +163,55 @@ export function syncExternalCliCredentials(
|
||||
|
||||
// Sync from Codex CLI
|
||||
const existingCodex = store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
const existingCodexOAuth = existingCodex?.type === "oauth" ? existingCodex : undefined;
|
||||
const duplicateExistingId = existingCodexOAuth
|
||||
? findDuplicateCodexProfile(store, existingCodexOAuth)
|
||||
: undefined;
|
||||
if (duplicateExistingId) {
|
||||
delete store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
mutated = true;
|
||||
log.info("removed codex-cli profile: credentials already exist in another profile", {
|
||||
existingProfileId: duplicateExistingId,
|
||||
removedProfileId: CODEX_CLI_PROFILE_ID,
|
||||
});
|
||||
}
|
||||
const shouldSyncCodex =
|
||||
!existingCodex ||
|
||||
existingCodex.provider !== "openai-codex" ||
|
||||
!isExternalProfileFresh(existingCodex, now);
|
||||
const codexCreds = shouldSyncCodex
|
||||
const codexCreds = shouldSyncCodex || duplicateExistingId
|
||||
? readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS })
|
||||
: null;
|
||||
if (codexCreds) {
|
||||
const existing = store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
const duplicateProfileId = findDuplicateCodexProfile(store, codexCreds);
|
||||
if (duplicateProfileId) {
|
||||
if (store.profiles[CODEX_CLI_PROFILE_ID]) {
|
||||
delete store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
mutated = true;
|
||||
log.info("removed codex-cli profile: credentials already exist in another profile", {
|
||||
existingProfileId: duplicateProfileId,
|
||||
removedProfileId: CODEX_CLI_PROFILE_ID,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const existing = store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
|
||||
// Codex creds don't carry expiry; use file mtime heuristic for freshness.
|
||||
const shouldUpdate =
|
||||
!existingOAuth ||
|
||||
existingOAuth.provider !== "openai-codex" ||
|
||||
existingOAuth.expires <= now ||
|
||||
codexCreds.expires > existingOAuth.expires;
|
||||
// Codex creds don't carry expiry; use file mtime heuristic for freshness.
|
||||
const shouldUpdate =
|
||||
!existingOAuth ||
|
||||
existingOAuth.provider !== "openai-codex" ||
|
||||
existingOAuth.expires <= now ||
|
||||
codexCreds.expires > existingOAuth.expires;
|
||||
|
||||
if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, codexCreds)) {
|
||||
store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds;
|
||||
mutated = true;
|
||||
log.info("synced openai-codex credentials from codex cli", {
|
||||
profileId: CODEX_CLI_PROFILE_ID,
|
||||
expires: new Date(codexCreds.expires).toISOString(),
|
||||
});
|
||||
if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, codexCreds)) {
|
||||
store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds;
|
||||
mutated = true;
|
||||
log.info("synced openai-codex credentials from codex cli", {
|
||||
profileId: CODEX_CLI_PROFILE_ID,
|
||||
expires: new Date(codexCreds.expires).toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,13 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||
import lockfile from "proper-lockfile";
|
||||
import { resolveOAuthPath } from "../../config/paths.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js";
|
||||
import { syncExternalCliCredentials } from "./external-cli-sync.js";
|
||||
import {
|
||||
AUTH_STORE_LOCK_OPTIONS,
|
||||
AUTH_STORE_VERSION,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
log,
|
||||
} from "./constants.js";
|
||||
import { findDuplicateCodexProfile, syncExternalCliCredentials } from "./external-cli-sync.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js";
|
||||
|
||||
@@ -330,7 +335,18 @@ export function ensureAuthProfileStore(
|
||||
}
|
||||
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||
return mergeAuthProfileStores(mainStore, store);
|
||||
const merged = mergeAuthProfileStores(mainStore, store);
|
||||
|
||||
// Keep per-agent view clean even if the main store has codex-cli.
|
||||
const codexProfile = merged.profiles[CODEX_CLI_PROFILE_ID];
|
||||
if (codexProfile?.type === "oauth") {
|
||||
const duplicateId = findDuplicateCodexProfile(merged, codexProfile);
|
||||
if (duplicateId) {
|
||||
delete merged.profiles[CODEX_CLI_PROFILE_ID];
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
|
||||
|
||||
Reference in New Issue
Block a user