Files
openclaw/src/agents/auth-profiles/external-cli-sync.ts
giulio-leone bbe6f7fdd9 fix(auth): protect fresher codex reauth state
- invalidate cached Codex CLI credentials when auth.json changes within the TTL window
- skip external CLI sync when the stored Codex OAuth credential is newer
- cover both behaviors with focused regression tests

Refs #53466

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 09:53:24 -07:00

140 lines
3.5 KiB
TypeScript

import {
readCodexCliCredentialsCached,
readQwenCliCredentialsCached,
readMiniMaxCliCredentialsCached,
} from "../cli-credentials.js";
import {
EXTERNAL_CLI_SYNC_TTL_MS,
QWEN_CLI_PROFILE_ID,
MINIMAX_CLI_PROFILE_ID,
log,
} from "./constants.js";
import type { AuthProfileStore, OAuthCredential } from "./types.js";
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
type ExternalCliSyncOptions = {
log?: boolean;
};
function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean {
if (!a) {
return false;
}
if (a.type !== "oauth") {
return false;
}
return (
a.provider === b.provider &&
a.access === b.access &&
a.refresh === b.refresh &&
a.expires === b.expires &&
a.email === b.email &&
a.enterpriseUrl === b.enterpriseUrl &&
a.projectId === b.projectId &&
a.accountId === b.accountId
);
}
function hasNewerStoredOAuthCredential(
existing: OAuthCredential | undefined,
incoming: OAuthCredential,
): boolean {
return Boolean(
existing &&
existing.provider === incoming.provider &&
Number.isFinite(existing.expires) &&
(!Number.isFinite(incoming.expires) || existing.expires > incoming.expires),
);
}
/** Sync external CLI credentials into the store for a given provider. */
function syncExternalCliCredentialsForProvider(
store: AuthProfileStore,
profileId: string,
provider: string,
readCredentials: () => OAuthCredential | null,
options: ExternalCliSyncOptions,
): boolean {
const existing = store.profiles[profileId];
const creds = readCredentials();
if (!creds) {
return false;
}
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
if (shallowEqualOAuthCredentials(existingOAuth, creds)) {
return false;
}
if (hasNewerStoredOAuthCredential(existingOAuth, creds)) {
if (options.log !== false) {
log.debug(`kept newer stored ${provider} credentials over external cli sync`, {
profileId,
storedExpires: new Date(existingOAuth!.expires).toISOString(),
externalExpires: Number.isFinite(creds.expires)
? new Date(creds.expires).toISOString()
: null,
});
}
return false;
}
store.profiles[profileId] = creds;
if (options.log !== false) {
log.info(`synced ${provider} credentials from external cli`, {
profileId,
expires: new Date(creds.expires).toISOString(),
});
}
return true;
}
/**
* Sync OAuth credentials from external CLI tools (Qwen Code CLI, MiniMax CLI, Codex CLI)
* into the store.
*
* Returns true if any credentials were updated.
*/
export function syncExternalCliCredentials(
store: AuthProfileStore,
options: ExternalCliSyncOptions = {},
): boolean {
let mutated = false;
if (
syncExternalCliCredentialsForProvider(
store,
QWEN_CLI_PROFILE_ID,
"qwen-portal",
() => readQwenCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
options,
)
) {
mutated = true;
}
if (
syncExternalCliCredentialsForProvider(
store,
MINIMAX_CLI_PROFILE_ID,
"minimax-portal",
() => readMiniMaxCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
options,
)
) {
mutated = true;
}
if (
syncExternalCliCredentialsForProvider(
store,
OPENAI_CODEX_DEFAULT_PROFILE_ID,
"openai-codex",
() => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
options,
)
) {
mutated = true;
}
return mutated;
}